【NOI 2015】程式自動分析 並查集與離散化處理
題目描述
在實現程式自動分析的過程中,常常需要判定一些約束條件是否能被同時滿足。
考慮一個約束滿足問題的簡化版本:假設x1,x2,x3,…代表程式中出現的變數,給定n個形如xi=xj或xi≠xj的變數相等/不等的約束條件,請判定是否可以分別為每一個變數賦予恰當的值,使得上述所有約束條件同時被滿足。例如,一個問題中的約束條件為:x1=x2,x2=x3,x3=x4,x1≠x4,這些約束條件顯然是不可能同時被滿足的,因此這個問題應判定為不可被滿足。
現在給出一些約束滿足問題,請分別對它們進行判定。
輸入
輸入檔案的第1行包含1個正整數t,表示需要判定的問題個數。注意這些問題之間是相互獨立的。
對於每個問題,包含若干行:
第1行包含1個正整數n,表示該問題中需要被滿足的約束條件個數。
接下來n行,每行包括3個整數i,j,e,描述1個相等/不等的約束條件,相鄰整數之間用單個空格隔開。若e=1,則該約束條件為xi=xj;若e=0,則該約束條件為xi≠xj。
輸出
輸出檔案包括t行。
輸出檔案的第k行輸出一個字串“YES”或者“NO”(不包含引號,字母全部大寫),“YES”表示輸入中的第k個問題判定為可以被滿足,“NO”表示不可被滿足。
測試輸入
2
2
1 2 1
1 2 0
2
1 2 1
2 1 1
測試輸出
NO
YES
提示
在第一個問題中,約束條件為:x1=x2,x1≠x2。這兩個約束條件互相矛盾,因此不可被同時滿足。
在第二個問題中,約束條件為:x1=x2,x2=x1。這兩個約束條件是等價的,可以被同時滿足。
1≤n≤1000000
1≤i,j≤1000000000
題目分析
對於本題,目的是判斷程式能否滿足所有的輸入,因為只有相同和不同兩種關係,那麼我們可以先處理所有的相同的關係,將相同的數對用並查集維護成一個個不相交集合或者相交集合,而後再處理不同的數對,一旦不同的數對在同一個集合中則發生矛盾,輸出NO即可,遍歷完所有的不同數對發現確實都屬於不同的集合則輸出YES,本題需要注意的是輸入數字較大,需要用離散化進行處理,直接開陣列是不行的
注意點
- 關於並查集的建立,本題由於資料量很大,所以需要用到路徑壓縮,註釋的部分是不採用路徑壓縮時的程式碼,路徑壓縮目的能使一個集合在與另一個集合合併前是以一棵深度只有1的樹,這樣減少了部分查詢的消耗(資料量大的時候更為明顯)
int find(int x){
if(p[x] == x) return x;
else{
p[x] = find(p[x]);
}
return p[x];
// while(p[x] != x){
// x = p[x];
// }
// return x;
}
- 關於C++ STL中的unique()函式,它的作用是對一個已經排序完成的陣列,實現“合併”相鄰的相同數字,為什麼合併要帶引號呢?事實上,合併後的陣列長度未發生變化,它合併的過程是不斷把後面的元素覆蓋前面重複元素(具體就不作講解了),在對陣列執行unique操作時,一般接受兩個引數,陣列的首地址和尾地址,而它的返回值是完成查重後的陣列的有序位最後一個+1的引用,就是說陣列1,1,2,2,4,5,5,6通過unique(a, a+8)之後陣列就變成了1,2,4,5,6,5,5,6只有前5個是有序的,且int t = unique(a, a+8)存放的是a[5]的引用,所以如果想得到具體是第幾個那麼我們一般寫成int t = unique(a, a + 8) - a;那麼t自然而然就是5,下面是程式碼
#include<iostream>
#include<algorithm>
#include<stdio.h>
using namespace std;
int main(){
int a[] = {1, 1, 2, 2, 4, 5, 5, 6};
int t = unique(a, a + 8) - a;
cout<<t<<endl;
cout<<a[t]<<endl;
for(int i = 0; i <= 7; i++){
cout<<a[i]<<endl;
}
return 0;
}
- 關於lower_bound()函式的使用,這是一個高效的方法用二分查詢陣列中第一次出現某個數的引用,它接收三個引數,陣列首地址,陣列尾地址,需要查詢的數的值,一般我們將查詢出的結果減去陣列名,則得到一個數組中下標為i的位置是第一次出現查詢的number的位置
i = lower_bound(a + 1, a + n + 1, number) - a;
- 離散化:因為資料量i,j很大,而最多有1000000對輸入,顯然最多有2000000個不同的數字,我們可以構建一個2000000大的陣列存放這所有的輸入的i,j(儘管它們本身可能大大超出2000000但放在這個數組裡卻沒有任何問題)
本題程式碼
#include<iostream>
#include<algorithm>
#include<stdio.h>
using namespace std;
const int N = 1000005;
int p[N<<1];
int m[N<<1];
int x[N];
int y[N];
int type[N];
int n, cnt, num;
struct Node{ //結構體是用於存放那些不同的數對的i j的第一次在陣列中出現的位置
int x, y;
}k[N<<1];
void merge(){ //合併重複元素,實際上是覆蓋操作,陣列長度未發生變化,temp存放最後一個有序位置的索引(因為-m-1)
sort(m + 1, m + num + 1);
int temp = unique(m + 1, m + num + 1) - m - 1;
for(int i = 1; i <= n; i++){
x[i] = lower_bound(m + 1, m + temp + 1, x[i]) - m; //lowerbound()前兩個引數的閉區間和開區間
y[i] = lower_bound(m + 1, m + temp + 1, y[i]) - m; //執行完成後x[i]存放m陣列中第一次出現x[i]的位置
}
}
int find(int x){
if(p[x] == x) return x;
else{
p[x] = find(p[x]);
}
return p[x];
// while(p[x] != x){
// x = p[x];
// }
// return x;
}
void Union(int x, int y){
int fx = find(x);
int fy = find(y);
if(fx != fy) p[fy] = fx;
}
int main(){
int t;
scanf("%d", &t);
while(t--){
scanf("%d", &n);
cnt = 0;
num = 0;
for(int i = 1; i <= n; i++){ //對輸入的n組輸入進行2*n的離散化處理
scanf("%d%d%d", &x[i], &y[i], &type[i]);
m[++num] = x[i];
m[++num] = y[i];
}
for(int i = 1; i <= num; i++) p[i] = i; //p陣列存放根
merge();
for(int i = 1; i <= n; i++){
if(type[i] == 1){
Union(x[i], y[i]); //type == 1時 將根合併同時路徑壓縮,這裡要注意,x[i] y[i]
}else{ //存放的是第一次出現位置,只有這樣才能在2000000空間中查詢1000000000大的數
k[++cnt].x = x[i]; //k結構體陣列存放type == 0時數對x[i],y[i]第一次在陣列m中出現的位置
k[cnt].y = y[i];
}
}
int flag = 0;
for(int i = 1; i <= cnt; i++){
int fx = find(k[i].x);
int fy = find(k[i].y);
if(fx == fy){ //如果根相同 則說明相等 矛盾
flag = 1;
printf("NO\n");
break;
}
}
if(flag == 0){
printf("YES\n");
}
}
return 0;
}