Description
現有N個動物,以1-N編號。每個動物都是A,B,C中的一種,但是我們並不知道它到底是哪一種。
有人用兩種說法對這N個動物所構成的食物鏈關係進行描述:
第一種說法是"1 X Y",表示X和Y是同類。
第二種說法是"2 X Y",表示X喫Y。
此人對N個動物,用上述兩種說法,一句接一句地說出K句話,這K句話有的是真的,有的是假的。當一句話滿足下列三條之一時,這句話就是假話,否則就是真話。
1) 當前的話與前面的某些真的話衝突,就是假話;
2) 當前的話中X或Y比N大,就是假話;
3) 當前的話表示X喫X,就是假話。
你的任務是根據給定的N(1 <= N <= 50,000)和K句話(0 <= K <= 100,000),輸出假話的總數。
Input
以下K行每行是三個正整數 D,X,Y,兩數之間用一個空格隔開,其中D表示說法的種類。
若D=1,則表示X和Y是同類。
若D=2,則表示X喫Y。
Output
Sample Input
100 7 1 101 1 2 1 2 2 2 3 2 3 3 1 1 3 2 3 1 1 5 5
Sample Output
3
Source
解法一 用帶權值的並查集
本題就是需要我們判斷給定的兩點是否滿足某種關係。同一顆樹裏面的任意兩節點間的關係我們是想知道的。
1。。根據向量如果已知A與b的關係(共只有三種關係),b與c的關係,那定會推出A與c的關係,可以在紙上畫畫,更清楚。據此,就能求得每個節點到其根節點的關係。
2。。 對於現在不在同一顆樹中元素,合併前需知道兩個子樹的根節點的關係。這在後面的推倒
上面兩個問題是解本題的核心!!!
之前並查集是處理的一類的元素操作,而該題元素之間有關係(同類,喫,被喫),因此對於每個元素除了需要保存每個元素的父親節點,還需保存於父親節點的關係。
之前的樹裏面代表任意兩個數在一個集合,而此問題代表任意兩個數的關係的以確認。
在進行樹與樹間合併時,是合併已經確定關係的元素,如現有根節點A的子樹 、根節點B的子樹如知道節點A 與節點b的關係就可合併。根據我們保存的信息我們可以推出任意節點與其根節點的關係,具體細節見代碼註釋。
補充:我看別人都說向量解答,剛開始有點懵,然後發現有點像相對位置,可以相加(特殊的規則),因此本問題可以擴展多個不同種類的動物。
#include<stdio.h>
#include<math.h>
#include<stdlib.h>
#include<algorithm>
#include<string.h>
#include<vector>
using namespace std;
int fa[50010];
int r[50010];
int ct;
void init(int n){
int i;
for(i=1;i<=n;i++)
{
fa[i] = i;
r[i] = 0;
}
}
int find(int x)
{
if(x != fa[x]){
int tmp = fa[x]; //注意,x的父親節點經過遞歸會改變,因此需臨時保留。
fa[x] = find(fa[x]);
r[x] = (r[tmp] + r[x])%3; //這了首先明白默認規則 r【i】的值爲0,代表 i節點與根節點同類
// r【i】的值爲2,代表 i節點被根節點喫
// r【i】的值爲1,代表 i節點喫根節點
//關係是通過列出所有情況所找出來的,情況不多,就十來種 (枚舉所有x取0,1,2,fa【x】(x父親)取0,1,2的情況)就能看出x與其祖父的關係)
//而並查集的UNION(合併) 是把能確定兩點關係的節點保存在一顆樹中
//如果我們知道任意兩點的關係,就能把他們放在同一顆樹中(因爲確定x--rx關係
//rx--ry關係 x--y關係)[可以在紙上畫畫更清晰]
}
return fa[x];
}
bool Union(int d,int x,int y)
{
int rx = find(x);
int ry = find(y);
//printf("x=%d %d %d %d ry=%d %d %d %d\n",x,rx,r[x],fa[x],y,ry,r[y],fa[y]);
if(rx == ry){ //已經知道x、y間的關係,直接判斷,去掉不合法
if( d==1 && r[x] != r[y])
return 1;
if(d==2)
{
/*if( (r[x] == 0 &&r[y]!=2) || (r[x] == 1 &&r[y]!=0)||
(r[x] == 2 &&r[y]!=1))
return 1;*/
if(r[x] == 2 && r[y] != 0) return 1;
if(r[x] == 1 && r[y] != 2) return 1;
if(r[x] == 0 && r[y] != 1) return 1;
}
return 0;
}
fa[rx] = ry;
r[rx] = (d-1 + r[y] - r[x] +3)%3; // ( ((d-1) +r[y]) + (3-r[x]) )%3
//把x所在的子樹連接到y的根節點上 (只需改x的根節點rx與y的根節點ry的關係)
// 公式的推倒,是難點!!! 首先我們已知x與y的關係(就是d的值),x與x的根節點的關係(即爲r[])
// 首先可以通過r【i】,r【fa】的值知道i與fa【fa【i】】的關係 這裏我們可以導出x與ry的關係(1)
// 然後還需推倒一下:通過r【i】的值,導出父親對孩子的關係, 這裏我們可以導出rx與x的關係(2)
//最後 在利用一下 1 的性質知道 rx與ry的關係 這裏我們可以導出rx與ry的關係
return 0;
}
int main()
{
int i,j,k,n,m,d,x,y,tmp;
ct=0;
scanf("%d%d",&n,&m);
init(n);
for(i=0;i<m;i++){
scanf("%d%d%d",&d,&x,&y);
if(d==2&&x==y || x>n || y>n)
{
//printf("i = %d \n",i);
ct++;continue;
}
tmp = Union(d,x,y);
//if(tmp){
//printf("i = %d \n",i);
//}
ct += tmp;
}
printf("%d\n",ct);
return 0;
}
法2
巧妙地解法,超出常規的思維。
把本題變種成一般的並查集。
同樣
本題就是需要我們判斷給定的兩點是否滿足某種關係。同一顆樹裏面的任意兩節點間的關係我們是想知道的。
該開始第一次看本題在《挑戰程序設計》中沒懂。。
此次瞭解了。
#include<stdio.h>
#include<math.h>
#include<stdlib.h>
#include<algorithm>
#include<string.h>
#include<vector>
using namespace std;
int fa[3*50010];
int r[3*50010];
int find(int x){
if(x != fa[x]){
fa[x] = find(fa[x]);
}
return fa[x];
}
int same(int x,int y){
if(find(x) == find(y))
return 1;
return 0;
}
void merge(int x,int y){ //一棵樹裏面的元素a,b,則代表有通過a能找到與b的關係,或不能通過a找到b的關係,具體什麼關係還看a與b的差值(幾倍n)
//小一倍數代表同類,小兩倍代表a可喫b,小三倍代表被b喫(此時的a是小於n)
x=find(x);y=find(y);
if(r[x] < r[y]){
fa[x] = y;
}
else {
if(r[x] == r[y]){
r[x] ++ ;
}
fa[y] = x;
}
}
int main()
{
int i,j,k,n,m;
int d,x,y;
int count = 0;
scanf("%d%d",&n,&k);
for(i=1;i<=3*n;i++) { //把每個元素變成3個,設第一個爲x,則第二個爲x+n,第三個 x+2n
fa[i] = i;r[i] = 0;
}
for(i=0;i<k;i++){
scanf("%d%d%d",&d,&x,&y);
if(x>n || y>n){
count++;
continue;
}
if(d==1){
if(same(x,y+n)||same(x,y+2*n)) //已知x y不是同一類 ,排除的情況
{
count++;
continue;
}
merge(x,y);
merge(x+n,y+n);
merge(x+2*n,y+2*n);
}
else {
if(same(x,y)||same(x,y+2*n)) //已知x 不能喫 y,,排除的情況
{
count++;
continue;
}
merge(x , y+n);
merge(x+n , y+2*n);
merge(x+2*n , y);
}
}
printf("%d\n",count);
return 0;
}