差分约束专题练习
就是一直解不等式的可行解的一种图论建模。
P5960 【模板】差分约束
对于式子:
求一组可行解。
如果有
因为可能有负权边之类的,只能跑一个 SPFA。注意判负环(重复进队超过总点个数次)。
const int N=5003; int n,m,t[N],dis[N]; bool vis[N]; vector<PII>f[N]; queue<int>Q; int main(){ n=read(),m=read(); for(int i=1;i<=n;i++)f[0].push_back({0,i}),dis[i]=1e9; for(int i=1;i<=m;i++){ int u=read(),v=read(),w=read(); f[v].push_back({w,u}); } Q.push(0); while(!Q.empty()){ int x=Q.front();Q.pop(); vis[x]=0; if(++t[x]>n)return puts("NO"),0; for(PII PI:f[x]){ int w=PI.first,y=PI.second; if(dis[y]>dis[x]+w){ dis[y]=dis[x]+w; if(!vis[y])Q.push(y),vis[y]=1; } } } for(int i=1;i<=n;i++)printf("%d ",dis[i]); return 0; }
P1250 种树
这题黄吗,我觉得如果用差分约束的话建模肯定比模板难一点吧。
因为是区域限制,我们不妨令最终的树前缀和为
发现还有约束:
const int N=3e4+5; int n,m,dis[N]; bool vis[N]; vector<PII>f[N]; queue<int>Q; int main(){ n=read(),m=read(); for(int i=0;i<=n;i++){ f[n+1].push_back({0,i});dis[i]=1e9; if(i!=n)f[i+1].push_back({0,i}),f[i].push_back({1,i+1}); } for(int i=1;i<=m;i++){ int u=read()-1,v=read(),w=read(); f[v].push_back({-w,u}); } Q.push(n+1); while(!Q.empty()){ int x=Q.front();Q.pop(); vis[x]=0; for(PII PI:f[x]){ int w=PI.first,y=PI.second; if(dis[y]>dis[x]+w){ dis[y]=dis[x]+w; if(!vis[y])Q.push(y),vis[y]=1; } } } cout<<dis[n]-dis[0]; return 0; }
2023.11.8
P1969 [NOIP2013 提高组] 积木大赛
把原来的序列转化为差分序列,然后建模。每次操作
为什么题解没有这种做法服了。
话说为什么这题会被我归到差分约束呢?正睿题单里翻到的。
P4926 [1007] 倍杀测量者
一道好题,让我发现原来这种题目也能用二分和图论建模写。
首先是这个二分,观察到
然后是如何建边,有两种不同的方法:
- 在原条件下跑最小(大)乘积路
- 在原条件下两边同时加个
跑最短(长)路。
先看第一种,
我们将操作一不穿女装的限制转化为
对于分数呢?我们一开始令源点
当然还有一个等效转化,我们上述限制如果均满足说明没有人需要穿女装,那么有人穿女装的条件自然就是有限制没有被满足。而限制没被满足意味着有一直变小和一直变大的乘积环,这里判断是否有点被加入队列
复杂度O(卡一卡能过)。
哦对了,对于加对数的转化应该也差不多,反正想到这题的转化这题就不难了。
P.S. 貌似是因为
//最长路 typedef long double ld; typedef pair<int,ld> PII; const int N=1003; struct edge{ int opt,x,y,k; }a[N]; int n,s,t,tim[N]; ld dis[N]; bool vis[N]; queue<int>Q; PII xx[N]; vector<PII>f[N]; bool check(ld k){ while(!Q.empty())Q.pop(); for(int i=0;i<=n;i++)dis[i]=1,vis[i]=1,Q.push(i),f[i].clear(),tim[i]=0; for(int i=1;i<=s;i++) if(a[i].opt&1) f[a[i].y].push_back({a[i].x,a[i].k-k}); else f[a[i].y].push_back({a[i].x,1/(ld)(a[i].k+k)}); for(int i=1;i<=t;i++) f[0].push_back({xx[i].first,xx[i].second}),f[xx[i].first].push_back({0,(ld)1.0/xx[i].second}); while(!Q.empty()){ int x=Q.front();Q.pop(); vis[x]=0; if(++tim[x]>n+1)return 1; for(PII PI:f[x]){ int y=PI.first;ld w=PI.second; if(dis[y]<dis[x]*w){ dis[y]=dis[x]*w; if(!vis[y])vis[y]=1,Q.push(y); } } } return 0; } int main(){ n=read(),s=read(),t=read(); ld l=0,r=1e9; for(int i=1;i<=s;i++){ int opt=read(),x=read(),y=read(),k=read(); a[i]={opt,x,y,k}; if(opt&1)r=min(r,(ld)k-1e-5); } for(int i=1;i<=t;i++){ int x=read(),w=read(); xx[i]={x,w}; } if(!check(0))return puts("-1"),0; while(r-l>1e-5){ ld mid=(l+r)/2; if(check(mid))l=mid; else r=mid; } printf("%.6LF\n",l); return 0; }
对数的就是在加边的时候套个 log2l()
,然后把乘号改加号,为减小篇幅就不放了。(细节:不要直接对分数取对数,应该在前面加个负号然后把分母放回来,不然精度比我NOIP2022成绩还低)。
P1993 小 K 的农场
就没有上面的转化直接建边就行了。
P2474 [SCOI2008] 天平
11.8 15:16
以前模拟赛出过差不多的题,但是当时不会,想不到是建模,现在学了来看一下是不是能写出来吧。
假设我们有 +
表示有关系 -
,关系 =
关系,?
,因为什么都不知道,但是重量有限定,所以有关系
有了这些关系之后,我们分别建两次图跑最大路和最短路找每个点的最大值和最小值,如果两者一样说明值确定,否则不确定。确定的值我们拿来枚举拿来用就行了(然而不是,只要最小值大于最大值就行了)。复杂度
调着调着不知道为什么第二个点死活过不了。
16:26
#include<bits/stdc++.h> using namespace std; typedef pair<int,int> PII; int n,A,B; vector<PII>f[2][52]; bool vis[52]; int a[2][52]; queue<int>Q; void SPFA(bool k){//k=0,shortest,k=1,longest for(int i=1;i<=n;i++)a[k][i]=(k?1:3),vis[i]=1,Q.push(i); while(!Q.empty()){ int x=Q.front(); vis[x]=0,Q.pop(); for(PII PI:f[k][x]){ int y=PI.first,w=PI.second; bool ok=(!k?(a[k][y]>a[k][x]+w):(a[k][y]<a[k][x]+w)); if(ok){ a[k][y]=a[k][x]+w; if(!vis[y])Q.push(y),vis[y]=1; } } } return; } inline bool C(int w){return a[0][w]==a[1][w];} int main(){ cin>>n>>A>>B;getchar(); for(int i=1;i<=n;i++,getchar()) for(int j=1;j<=n;j++){ char op=getchar(); if(i<=j)continue; if(op=='+') f[0][i].push_back({j,-1}),f[1][j].push_back({i,+1}); if(op=='-') f[0][j].push_back({i,-1}),f[1][i].push_back({j,+1}); if(op=='=') f[0][i].push_back({j,+0}),f[0][j].push_back({i,+0}), f[1][i].push_back({j,+0}),f[1][j].push_back({i,+0}); } SPFA(0),SPFA(1); for(int k=0;k<3;k++){ int ans=0; for(int i=2;i<=n;i++) for(int j=1;j<i;j++){ if(i==A||j==A||i==B||j==B)continue; if(!k)//A+B>i+j,说明a+b最小值大于i+j最小值 ans+=(a[1][A]+a[1][B]>a[0][i]+a[0][j]); else if(k&1)//相同,那说明a+b和 i+j都得确定 ans+=(C(A)&C(B)&C(i)&C(j)&(a[1][A]+a[1][B]==a[1][i]+a[1][j])); else ans+=(a[0][A]+a[0][B]<a[1][i]+a[1][j]); } printf("%d ",ans); } return 0; }
样例是过了,这时发现其他点都过不了,然后我发现,我虽然能求出每个点的上下界,可是如果你直接粗略地拿具体值去比的话,只能得到必要条件不能得到全部条件,就比如人家可能确实存在
这个错误也是我在题解里找的吧,然后发现我们要求的是
P3084 [USACO13OPEN] Photo G
又是一个差分式的建模。
18:52
发现果然不行,数据太大了,开 O2 都只能拿 70,想想怎么优化。
显然差分约束不是正解了,吗?
我们用类似于 dijkstra 的贪心,把队列换成优先队列,然后跑 SPFA,但是这个时候还是会 T 一个点,因为负环很难搞,怎么办呢?因为负环要跑很久,所以我们直接卡时即可通过本题,复杂度O(能过)。
考试的时候还是不要用优先队列了,有可能被卡飞起来,我们可以用一个双端队列的玄学方法代替:当当前加入值小于队首时前插,否则后插。
typedef pair<int,int> PII; const int N=2e5+5;//双端队列卡时版 int n,m,dis[N]; bool vis[N]; deque<int>Q; vector<PII>f[N]; int main(){ int cl=clock(); n=read(),m=read(); for(int i=1;i<=m;i++){ int u=read()-1,v=read(); f[v].push_back({u,-1}); f[u].push_back({v,1}); } for(int i=1;i<=n;i++) f[i-1].push_back({i,1}),f[i].push_back({i-1,0}); memset(dis,0x3f,sizeof(dis)); dis[0]=0; Q.push_back(0); while(!Q.empty()){ int x=Q.front();Q.pop_front();vis[x]=0; if(clock()-cl>=0.9*CLOCKS_PER_SEC)return puts("-1"),0; for(PII PI:f[x]){ int y=PI.first,w=PI.second; if(dis[y]>dis[x]+w){ dis[y]=dis[x]+w; if(!vis[y]){ vis[y]=1; if(!Q.empty()&&dis[y]>dis[Q.front()])Q.push_back(y); else Q.push_front(y); } } } } printf("%d",dis[n]-dis[0]); return 0; }
虽然是在打差分约束,但是还是想一想正解吧,毕竟这么卡时加玄学优化真的有点不人性。
假设我们一定要在一个地方放牛的话,我们尽可能保证之前的那只靠的足够地近,用
完了现在想不来,挖个坑吧。
P7624 [AHOI2021初中组] 地铁
(做为题解写的)
看到这个题目首先想到差分约束,但是环上问题比较麻烦。
我们首先想到断环成链,然后新加一些限制条件,然而发现这些限制条件都是多元的,无法用差分约束的形式表述。
我们重新回到原条件,直接在环上试试?
我们令
那么如果我们有限制条件
- 当
,即 。 - 当
,相当于我们只有 的点权没有被取,那么限制就是 ,移项得 。
同理第二种限制条件。
而我们不难发现这个也是多元的,只不过很多时候会与这个
P3275 [SCOI2011] 糖果
11.9 15:32
这题的约束非常好写,写出来最长路的形式(因为要求最小值所以必须用最长路的形式)之后,跑了个卡时 SPFA 判正环并通过了样例,提交一看
然后开始思考正解。发现每条边都只可能边为 可以跑dij只要一个强连通分量上存在
const int N=1e5+5; int n,m,st,dp[N],tar[N],Tar,sum[N],dfn[N],low[N],cnt,inn[N]; vector<PII>g[N]; vector<int>f[2][N]; bool vis[N]; stack<int>S; queue<int>Q; void Tarjan(int x){ dfn[x]=low[x]=++cnt;vis[x]=1;S.push(x); for(int y:f[0][x]) if(!dfn[y])Tarjan(y),low[x]=min(low[x],low[y]); else if(vis[y])low[x]=min(low[x],low[y]); if(dfn[x]==low[x]){ int y;Tar++; do{ y=S.top(); tar[y]=Tar; sum[Tar]++; S.pop(); vis[y]=0; }while(y^x); } } int main(){ n=read(),m=read(),st=clock(); for(int i=1;i<=m;i++){ int opt=read(),u=read(),v=read(); if(opt==1)f[0][u].push_back(v),f[0][v].push_back(u); if(opt==2)f[1][u].push_back(v); if(opt==3)f[0][v].push_back(u); if(opt==4)f[1][v].push_back(u); if(opt==5)f[0][u].push_back(v); } for(int i=1;i<=n;i++)if(!dfn[i])Tarjan(i); for(int x=1;x<=n;x++){ for(int y:f[0][x]) if(tar[x]!=tar[y])g[tar[x]].push_back({tar[y],0}),inn[tar[y]]++; for(int y:f[1][x]) if(tar[x]!=tar[y])g[tar[x]].push_back({tar[y],1}),inn[tar[y]]++; else return puts("-1"),0;//必须判 } for(int i=1;i<=Tar;i++)if(!inn[i])Q.push(i); while(!Q.empty()){ int x=Q.front();Q.pop(); for(PII PI:g[x]){ int y=PI.first,w=PI.second; dp[y]=max(dp[y],dp[x]+w); if(!--inn[y])Q.push(y); } } for(int i=1;i<=Tar;i++)if(inn[i])return puts("-1"),0;//必须判 ll ans=n; for(int i=1;i<=Tar;i++)ans+=1ll*sum[i]*dp[i]; cout<<ans; return 0; }
那如果直接缩点就没有这个烦恼了,因为出来绝对是 DAG,只需要判一下是否存在强连通内部的点互相连
void Tarjan(int x){ for(PII PI:f[x]) //... } int main(){ n=read(),m=read(),st=clock(); for(int i=1;i<=m;i++){ int opt=read(),u=read(),v=read(); if(opt==1)f[u].push_back({v,0}),f[v].push_back({u,0}); if(opt==2)f[u].push_back({v,1}); if(opt==3)f[v].push_back({u,0}); if(opt==4)f[v].push_back({u,1}); if(opt==5)f[u].push_back({v,0}); } for(int i=1;i<=n;i++)if(!dfn[i])Tarjan(i); for(int x=1;x<=n;x++){ for(PII PI:f[x]){ int y=PI.first,w=PI.second; if(tar[x]!=tar[y])g[tar[x]].push_back({tar[y],w}),inn[tar[y]]++; else if(tar[x]==tar[y]&&w)return puts("-1"),0; } } //... }
本文作者:NBestの思潮
本文链接:https://www.cnblogs.com/NBest/p/17818767.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步