這兩天做了幾道並查集的題目,hdu的聯通工程啊more is better 啊,然后卡在hdu1829,帶權的並查集,沒搞懂,嘗試寫下來讓思路清晰些。
並查集是一種維護不同集合,在此基礎上實現快速判斷,統計個數等等的算法。
基礎的有find和join兩個功能,其中join作用於接收新數據。
並查集應用場景的幾個特點:
1.數據之間存在聯系;
2.借由兩個數據的聯系,數據會被雙向聯通的結合成幾個集合;
下面介紹下find和join的基本形式
const int maxn=1000; int p[maxn];//用於儲存數據的根 void init(int n) { for(int i=0;i<n;i++) { p[i]=i; } } int find(int x) { int t=x; while(p[t]!=t) t=p[t]; int i=x; while(p[i]!=i) { int tem=p[i]; p[i]=t; i=tem; } return t; }
這種是將關系優化避免出現樹變成一條路
還有一種如下
int find(int x) { if(p[x]==x)return x; int tem=find(p[x]); return tem; }
這種用遞歸的方法算
join函數:
void join(int x,int y) { int f1=find(x),f2=find(y); if(f1!=f2) { p[f1]=f2;
} }
知道了這些,一些水題就可以做了
接着是在這個基礎上的變化,如統計集合個數,一開始的想法是維護完數據后for一遍,實際這里可以發現,在p[]數組中,作為根節點的數據p[i]==i的,沒有必要通過鏈表結構特點去找根節點。接着處理掉這個for循環:
再看看現在的思路,是通過遍歷一遍p[]數組,統計滿足p[i]==i的個數,假設為a。如果所有數據沒有聯通,那么集合個數為n,a=n-(原本p[i]==i的點的p[i]改變的次數),即在join函數中每次尋找到兩個數據的根數據進行合並時加一個計數器,然后用n減去。可以這樣實現:
int ans=n; void join(int x,int y) { int f1=find(x),f2=find(y); if(f1!=f2) { p[f1]=f2; ans--; } }
接着是找集合中元素個數的題1856,這個可以通過增加一個初始化為1的數組,每次合並同時對應合並,同時max取最大值
#include<iostream> #include<string> #include<cstdio> #include<iomanip> #include<cstring> #include<cmath> #include<algorithm> #include<vector> #include<queue> #include<functional> #include<map> const int maxn = 10000005; #pragma warning(disable:4996) using namespace std; // freopen("john.in", "r", stdin); // freopen("john.out", "w", stdout); int p[maxn]; int n[maxn]; void pre() { for (int i = 1;i < maxn;i++) { p[i] = i; n[i] = 1; } } int find(int x) { int r = x; while (r != p[r]) { r = p[r]; } int i = x; while (i != p[i]) { int j = p[i]; p[i] = r; i = j; } return r; } void join(int x, int y,int& ans) { int tem1 = find(x), tem2 = find(y); if (tem1 != tem2) { p[tem1] = tem2; n[tem2] += n[tem1]; ans = max(n[y], ans); } } int main() { int t; while (~scanf("%d", &t)) { if (t == 0)puts("1"); else { int ans = 0; pre(); while (t--) { int i, j; scanf("%d%d", &i, &j); join(i, j, ans); } printf("%d\n", ans); } } }
還有一個應用是判斷是否成環,如果join的兩個數據p[]相同,則標記為成環,如1272這題,還要判斷唯一性,可以用前面的方法,也可以通過圖論中邊數為定點數減一來判斷
#include<iostream> #include<string> #include<cstdio> #include<iomanip> #include<cstring> #include<cmath> #include<algorithm> #include<vector> #include<queue> #include<functional> #include<map> const int maxn = 100005; #pragma warning(disable:4996) using namespace std; // freopen("john.in", "r", stdin); // freopen("john.out", "w", stdout); int p[maxn]; int flag; bool s[maxn]; int find(int a) { while (a != p[a]) { a = p[a]; } return a; } void join(int a, int b) { int i = find(a), j = find(b); if (i != j) { p[i] = p[j]; } else flag = 0; } void pre() { for (int i = 1;i < maxn;i++) { p[i] = i; s[i] = false; } } int main() { int a, b; while (cin >> a >> b) { if (a == -1 && b == -1)break; if (a == 0 && b == 0)printf("Yes\n"); else { flag = 1; pre(); s[a] = true; s[b] = true; join(a, b); while (scanf("%d%d", &a, &b), (a || b)) { s[a] = true; s[b] = true; join(a, b); } if (flag) { int cnt = 0; for (int i = 1;i < maxn;i++) { if (s[i] && p[i] == i) { cnt++; } if (cnt > 1) { flag = 0; break; } } } if (flag)printf("Yes\n"); else printf("No\n"); } } }
然后是1198這題,這題沒有顯示的給出數據間的關系,需要對數據進行處理,但並查集的部分沒有什么新東西
#include<iostream> #include<string> #include<cstdio> #include<iomanip> #include<cstring> #include<cmath> #include<algorithm> #include<vector> #include<queue> #include<functional> #include<map> const int maxn = 550; #pragma warning(disable:4996) using namespace std; // freopen("john.in", "r", stdin); // freopen("john.out", "w", stdout); //上下左右分別為0123 int list[11] = { 10,9,6,5,12,3,11,14,7,13,15 }; char a[maxn][maxn]; int ans; int n, m; int p[maxn*maxn+1]; void pre(int n) { for (int i = 0;i < n;i++) { p[i] = i; } } int find(int x) { int r = x; while (r != p[r]) { r = p[r]; } return r; } void judge(int ai, int aj, int bi, int bj) { if (bi >= m || bj >= n)return; bool flag = false; int t1 = a[ai][aj] - 'A'; int t2 = a[bi][bj] - 'A'; if (ai == bi&&aj < bj) { if (((list[t1] ) & 1) && ((list[t2]>>1) & 1))flag = true; } else if (aj == bj&&ai < bi) { if(((list[t1] >> 2) & 1) && ((list[t2]>>3) & 1))flag = true; } if (flag) { int f1 = find(ai*n + aj), f2 = find(bi*n + bj); if (f1 != f2) { p[f1] = f2; ans--; } } } int main() { //freopen("in.txt", "r", stdin); while (scanf("%d%d", &m, &n)!= EOF) { if (m == -1 || n == -1)break; pre(m*n); ans = n*m; for (int i = 0;i < m;i++) { scanf("%s", a[i]); } for (int i = 0;i < m;i++) { for (int j = 0;j < n;j++) { judge(i, j, i, j + 1); judge(i, j, i + 1, j); } } printf("%d\n", ans); } }
然后是現在做的這題,關於帶權並查集,還沒有弄明白,1829,題意大概是選擇動物,給出兩個數據的性別相反,問是否出現前后矛盾的情況,上網看到一種解法,思路是開兩個數組,每次接收數據,交換p[]中兩個數據對應的值,如果有處理到兩個數據的p[]相同,則錯誤。還有一種是帶權並查集。做了一個晚上帶權那種,還是有點沒搞明白。
帶權並查集多一個權值的數組,通過遞歸的find函數找到根節點。現在的問題是,每次找到根節點的過程和合並的過程,會讓原本a->b->c的關系變成a->c,b->c每次更新后都要再次調整權值數組。網上的解釋是把權值看成向量。例如sign[i]表示由i指向前一個節點的向量,那么改變后的指向由a->c,等於sign[a]+sign[p[a]]。
下面是代碼:
#include<iostream> #include<string> #include<cstdio> #include<iomanip> #include<cstring> #include<cmath> #include<algorithm> #include<vector> #include<queue> #include<functional> #include<map> #pragma warning(disable:4996) using namespace std; // freopen("john.in", "r", stdin); // freopen("john.out", "w", stdout); const int maxn = 2002; int p[maxn]; int sign[maxn]; int find(int x) { if (p[x] == x) { return x; } int tem = p[x]; p[x] = find(p[x]); sign[x] = (sign[x] + sign[tem]) % 2;//用向量的思想來解釋這個過程 return p[x]; } void unit(int n) { for (int i = 1;i <= n;i++) { p[i] = i; sign[i] = 0; } } void join(int x, int y) { int f1 = find(x); int f2 = find(y); if (f1 != f2) { p[f1] = f2; sign[f1] = (1 + sign[y] - sign[x]) % 2; } find(x); } int main() { freopen("in.txt", "r", stdin); int t; scanf("%d", &t); int cnt = 1; while (t--) { int n, m; scanf("%d%d", &n, &m); unit(n); bool flag = true; while (m--) { int i, j; scanf("%d%d", &i, &j); if(flag)join(i, j); if (sign[i] == sign[j]) { flag = false; } } printf("Scenario #%d:\n", cnt++); if (flag)printf("No suspicious bugs found!\n"); else printf("Suspicious bugs found!\n"); printf("\n"); } }
現在還有個地方沒有很清楚,就是在join函數中的sign[f1]的更新,為什么可以直接由sign[x]和sign[y]關系推到?
想通了!在find的過程中已經讓x鏈接到根節點了
2017/4/26
今天學習了poj那道經典的食物鏈,有三個狀態,看了結題報告,有兩個細微不同的思路,一個是在狀態數組sign里存0,1,2代表被吃,同類,吃的關系,另一種是在sign中存三種動物的種類,區別在更新sign數組的表達式上似乎有所不同。
通過這道題,一方面加深了關於向量關系的理解,另一方面是感覺數設的卡諾圖化簡作為二進制碼邏輯關系的化簡,這種思路在這種題里意外的有用【捂臉】
#include<iostream> #include<string> #include<cstdio> #include<iomanip> #include<cstring> #include<cmath> #include<algorithm> #include<vector> #include<queue> #include<functional> #include<map> #pragma warning(disable:4996) using namespace std; // freopen("john.in", "r", stdin); // freopen("john.out", "w", stdout); const int maxn = 50002; int p[maxn]; int sign[maxn]; void init(int n) { for (int i = 1;i <= n;i++) { p[i] = i; sign[i] = 0; } } int find(int x) { if (x == p[x])return x; int tem = p[x]; p[x] = find(p[x]); sign[x] = (sign[x] + sign[tem]) % 3; return p[x]; } bool join(int x, int y, int d) { int f1 = find(x), f2 = find(y); if (f1 == f2) { if (d == 1 && sign[x] != sign[y])return false; if (d == 2) { if (sign[x] == 2 && sign[y] != 1)return false; if (sign[x] == 0 && sign[y] != 2)return false; if (sign[x] == 1 && sign[y] != 0)return false; } return true; } p[f1] = f2; if (d == 1) { sign[f1] = (sign[y] - sign[x] + 3) % 3; } if (d == 2) { sign[f1]=(sign[y] - sign[x] + 1 + 3) % 3; } find(x);//看看是否必要,可以ac,還是保留吧 return true; } int main() { // freopen("in.txt", "r", stdin); int n, k; scanf("%d%d", &n, &k); init(n); int ans = 0; while (k--) { int d, x, y; scanf("%d%d%d", &d, &x, &y); if (x > n || y > n) { ans++; continue; } if (x == y&&d==2) { ans++; continue; } if (!join(x, y, d))ans++;//小心添加錯的關系而對正確的關系造成破壞 //判斷還是在函數中好,因為在這里會重復做過的事,找到find(x),(y)然后討論sign[x]sign[y]的關系 } printf("%d\n", ans); }
