Luogu P1640 [SCOI2010]連續攻擊遊戲
思路
首先要提一下的就是,這道題有很多種做法,比如說有二分圖匹配、並查集、貪心、搜尋等等。出於時間原因,我這裡只寫二分圖匹配和並查集寫法。
一、二分圖匹配(匈牙利演算法)
這道題的這種做法思路比較難想(但也沒有那麼困難)。比較容易想到的就是把每個物品的兩個屬性作為兩邊的點,然後搞二分圖匹配。但是這樣做並不好做(我不清楚這樣該怎麼做)。
那我們可以考慮換一種思維方式。
我們可以對裝置和其屬性分別建點,然後從每個屬性值分別向它所屬的裝備連有向邊(注意一定是有向邊,否則會出現全部都可以匹配的錯誤結果),然後在建好的二分圖上跑匈牙利演算法。
在統計答案時,我們從1開始依次列舉屬性值,然後跑匈牙利,判斷是否能夠匹配。只要出現第一個不能匹配的情況,終止列舉,因為屬性值必須要求連續。二分圖匹配的主要思路就這些,如果不能理解,
可以多舉幾個例子分析一下,就很好懂了(這裡因為我懶,就不提供分析了)。
最後就是一個小優化。由於每次跑匈牙利的時候都要清空vis陣列,每次都清空會導致時間複雜度上升(不過好像也能過,我沒試過)。因此我們可以將vis陣列定義為int型別,再使用一個時間戳tot,就是
相當於每次把表示是否匹配的0,1換成了tot-1和tot。這樣不會對程式造成影響,而且可以小幅壓縮時間。
Code
#include<iostream> #include<cstdio> #include<algorithm> #include<cstring> #include<cmath> #define MAXN 1000009 int n, lk[MAXN], res; //lk[i]=j 表示第i個點與第j個點匹配 int head[MAXN], cnt; int vis[MAXN], tot;//tot是時間戳 struct node{ int nxt, to; } edge[MAXN << 1]; inline void add_edge(int x,int y){ ++cnt; edge[cnt].nxt = head[x]; edge[cnt].to = y; head[x] = cnt; return; }//建圖 int DFS(int k){ if(vis[k]==tot) return 0;//若已經被匹配過且沒有其他方案,返回 0 vis[k] = tot;//打上時間戳標記 for (int i = head[k]; i;i=edge[i].nxt){//遍歷圖 int v = edge[i].to; if(lk[v]==0||DFS(lk[v])){//若當前點沒有被匹配過或者已匹配的點有其他方案可以選擇,即有增廣路,改變方案 lk[v] = k;//將v與k匹配 return 1;//此時一定可以匹配,返回1 } } return 0; } int main(){ scanf("%d", &n); for (int i = 1; i <= n;++i){ int u = 0, v = 0; scanf("%d%d", &u, &v); add_edge(u, i);//從兩個屬性值向武器編號連邊 add_edge(v, i); } for (int i = 1; i <= 10000;++i){ ++tot;//時間戳 if(DFS(i)) ++res;//嘗試匹配,若可行,累加答案 else break;//否則直接結束迴圈 } printf("%d\n", res); return 0; }
二、並查集做法
並查集做法的時間效率比二分圖差不了多少,但是同樣不太好理解。
對於每個資料,我們在每個武器的屬性值之間連邊,這樣到最後會出現一些連通塊。這些連通塊存在的形式有2種,一種是樹(不是環),另一種是環。
對於呈樹的狀態的連通塊,對於此題,即一定存在一種情況,使得只有一個點不被取到。而面對最大連續的要求,很明顯去取不到的那個點一定是當前連通塊中權值最大的那個點。
對於呈環的形態的連通塊,很明顯當前環上所有的點全部都能被取到。
那麼我們就可以在合併時使用flag陣列來維護這個性質。在每次讀入後,在每個武器的兩個屬性值之間連邊。
然後就是一種情況是合併這兩個點所在的連通塊(注意是要把權值小的連通塊合併到大的連通塊裡面),然後把權值小的連通塊的flag賦值為1。
如果不是上述情況(比如這兩個點已經在一個連通塊裡了),那麼我們就讓這個連通塊的頂點(即根節點)的flag賦值為1。
這樣做的話,對於一個大小為m的連通塊,若由恰好m-1條邊構成(即為樹),可以保證最大點的flag為0;若由大於等於m條邊構成(即為環),可以保證所有點的flag均為1。這樣最後只要按照權值從小
到大遍歷flag陣列,和匈牙利演算法類似得出結果(同樣證明例子我就不寫了,我有點懶)。
Code
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define MAXN 1000009
int n, f[MAXN], res;
int flag[MAXN];
inline int get_father(int k){
return k == f[k] ? k : f[k] = get_father(f[k]);
}//路徑壓縮的並查集寫法
inline void _init(void){
for (int i = 1; i <= n + 1; ++i)
f[i] = i;
return;
}//初始化函式
inline void _swap(int &x,int &y){
int tmp = x;
x = y, y = tmp;
return;
}//交換兩個變數的值
int main(){
scanf("%d", &n);
_init();
for (int i = 1; i <= n;++i){
int u = 0, v = 0;
scanf("%d%d", &u, &v);
u = get_father(u), v = get_father(v);
if(u==v) flag[u] = 1;//若是同一個點,flag=1
else{//嘗試合併
if(u>v) _swap(u, v);//要保證u<v
if(!flag[u]) flag[u] = 1;//把權值小的賦值為1
else flag[v] = 1;//否則再把權值大的賦值為1
f[u] = v;//把u(權值小的)併到v(權值大的)上
}
}
for (int i = 1; i <= n + 1; ++i){
if(flag[i]==0){
res = i - 1;//這裡得出結果的方法和二分圖寫法類似
break;
}
}
printf("%d\n", res);
return 0;
}