「BZOJ1202」[HNOI2005] 狡猾的商人
Description
刁姹接到一个任务,为税务部门调查一位商人的账本,看看账本是不是伪造的。账本上记录了n个月以来的收入情况,其中第i 个月的收入额为Ai(i=1,2,3…n-1,n), 。当 Ai大于0时表示这个月盈利Ai 元,当 Ai小于0时表示这个月亏损Ai 元。所谓一段时间内的总收入,就是这段时间内每个月的收入额的总和。 刁姹的任务是秘密进行的,为了调查商人的账本,她只好跑到商人那里打工。她趁商人不在时去偷看账本,可是她无法将账本偷出来,每次偷看账本时她都只能看某段时间内账本上记录的收入情况,并且她只能记住这段时间内的总收入。 现在,刁姹总共偷看了m次账本,当然也就记住了m段时间内的总收入,你的任务是根据记住的这些信息来判断账本是不是假的。
Input
第一行为一个正整数w,其中w < 100,表示有w组数据,即w个账本,需要你判断。每组数据的第一行为两个正整数n和m,其中n < 100,m < 1000,分别表示对应的账本记录了多少个月的收入情况以及偷看了多少次账本。接下来的m行表示刁姹偷看m次账本后记住的m条信息,每条信息占一行,有三个整数s,t和v,表示从第s个月到第t个月(包含第t个月)的总收入为v,这里假设s总是小于等于t。
Output
包含w行,每行是true或false,其中第i行为true当且仅当第i组数据,即第i个账本不是假的;第i行为false当且仅当第i组数据,即第i个账本是假的。
Sample Input
2
3 3
1 2 10
1 3 -5
3 3 -15
5 3
1 5 100
3 5 50
1 2 51
3 3
1 2 10
1 3 -5
3 3 -15
5 3
1 5 100
3 5 50
1 2 51
Sample Output
true
false
false
这道题是兴dalao推荐给我的,这道题如果不告诉你是并查集,你死也想不到。
一共两种解法:带权并查集和差分约束系统
一、差分约束系统
其实这道题如果用差分约束来写的话,不是特别难,它用了一个非常常用的思想:前缀和(类似于种树那道题),因为输入的账单是区间形式的,所以直接记录成前缀和,建双向边跑一边最长路,当存在正环的时候,就不合法了。
1 #include<stdio.h> 2 #include<string.h> 3 #include<queue> 4 #include<algorithm> 5 using namespace std; 6 const int inf=1e9+7; 7 template<class T>void read(T &x) 8 { 9 int f=0;x=0;char ch=getchar(); 10 while(ch<'0'||ch>'9') {f|=(ch=='-');ch=getchar();} 11 while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} 12 x=f?-x:x; 13 } 14 struct Edge{ 15 int v,w,next; 16 }e[7007]; 17 int head[7007],n,m,T,s,t,v,tot,d[1007],Tot[1007]; 18 bool vis[1007],flag; 19 void add(int u,int v,int w) 20 { 21 e[++tot].v=v; 22 e[tot].w=w; 23 e[tot].next=head[u]; 24 head[u]=tot; 25 } 26 bool spfa(int s) 27 { 28 for(int i=1;i<=n;++i) 29 d[i]=-inf; 30 queue<int> qwq; 31 qwq.push(s); 32 d[s]=0;vis[s]=1; 33 while(!qwq.empty()) 34 { 35 int u=qwq.front(); 36 qwq.pop(); 37 vis[u]=0; 38 if(++Tot[u]>n) return 0; 39 for(int i=head[u];i;i=e[i].next) 40 { 41 int v=e[i].v,w=e[i].w; 42 if(d[v]<d[u]+w) 43 { 44 d[v]=d[u]+w; 45 if(!vis[v]) 46 { 47 vis[v]=1; 48 qwq.push(v); 49 } 50 } 51 } 52 } 53 return 1; 54 } 55 int main() 56 { 57 read(T); 58 while(T--) 59 { 60 flag=0;tot=0; 61 memset(vis,0,sizeof(vis)); 62 memset(Tot,0,sizeof(Tot)); 63 memset(head,0,sizeof(head)); 64 read(n),read(m); 65 for(int i=1;i<=m;++i) 66 { 67 read(s),read(t),read(v); 68 add(s-1,t,v),add(t,s-1,-v); 69 } 70 for(int i=0;i<=n;++i) 71 if(!Tot[i]) 72 if(!spfa(i)) 73 {printf("false\n"),flag=1;break;} 74 if(!flag) printf("true\n"); 75 } 76 return 0; 77 }
二、带权并查集
同样的,题目给出了区间就用前缀和来写:如果知道sum(a,b)、sum(b,c),那我们只需要知道一组sun(a,c)便可以验证账本的真假了;所以用并查集维护点对(l,r),用一个前缀和数组维护权值w,每次输入点对(l,r)时,判断是否在并查集中,如果在就直接用前缀和查询是否合法,否则加入并查集;
PS:带权并查集必须用递归实现,否则会咕咕咕???
1 #include<stdio.h> 2 #include<string.h> 3 #include<algorithm> 4 using namespace std; 5 template<class T>void read(T &x) 6 { 7 int f=0;x=0;char ch=getchar(); 8 while(ch<'0'||ch>'9') {f|=(ch=='-');ch=getchar();} 9 while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} 10 x=f?-x:x; 11 } 12 int fa[107],cha[107],m,n,l,r,w,T; 13 14 int find(int x) 15 { 16 int xx=x,t; 17 while(fa[x]!=x) x=fa[x],cha[x]+=cha[fa[x]]; 18 while(xx!=x) 19 { 20 t=fa[xx]; 21 fa[xx]=x; 22 xx=t; 23 } 24 return x; 25 }/* 26 int find(int x) 27 { 28 if(x!=fa[x]) 29 { 30 int t=find(fa[x]); 31 cha[x]+=cha[fa[x]]; 32 fa[x]=t; 33 } 34 return fa[x]; 35 } */ 36 int main() 37 { 38 read(T); 39 while(T--) 40 { 41 bool flag=0; 42 read(n),read(m); 43 for(int i=0;i<=n;++i) 44 fa[i]=i,cha[i]=0; 45 for(int i=1;i<=m;++i) 46 { 47 read(l),read(r),read(w); 48 --l; 49 if(find(l)!=find(r)) 50 { 51 cha[fa[r]]=cha[l]-cha[r]-w; 52 fa[fa[r]]=fa[l]; 53 } 54 else if(cha[l]-cha[r]!=w) {printf("false\n");flag=1;break;} 55 } 56 if(!flag) printf("true\n"); 57 } 58 }
三、总结
这道题可以用以上两种解法来解决,但是各有特点
差分:时间较慢、代码量较大,但容易想到而且代码实现相对简单。
并查集:时间很快、代码量很小,但是相对不太容易想到思路
共同点:核心都是用了前缀和的思想,所以说前缀和(差分数组)虽然思想简单,但还是挺重要的一个内容,所以多做多练练吧