差分约束专题练习

就是一直解不等式的可行解的一种图论建模。

P5960 【模板】差分约束

对于式子:

\[\begin{cases} x_{c_1}-x_{c'_1}\leq y_1 \\x_{c_2}-x_{c'_2} \leq y_2 \\ \cdots\\ x_{c_m} - x_{c'_m}\leq y_m\end{cases} \]

求一组可行解。

如果有 \(x_1-x_2\le c\),移项得:\(x_1+c\ge x_2\),那么如果此时我们从一个万能源点出发与所有值连边,然后把 \(x_1 \to x_2\) 权值为 \(c\),如果跑最短路我们肯定可以得到一个最小的 \(x_2\) 满足所有条件。

因为可能有负权边之类的,只能跑一个 SPFA。注意判负环(重复进队超过总点个数次)。

\(Code\)

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 种树

这题黄吗,我觉得如果用差分约束的话建模肯定比模板难一点吧。

因为是区域限制,我们不妨令最终的树前缀和为 \(sum_i\),那题目给的限制转化为 \(sum_r-sum_{l-1}\ge t\),移项得 \(sum_r-t\ge sum_{l-1}\),最后其实就是求 \(sum_n-sum_0\) 的最小值。

发现还有约束:\(0\le sum_i-sum_{i-1}\le 1\)

\(Code\)

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 提高组] 积木大赛

把原来的序列转化为差分序列,然后建模。每次操作 \([L,R]\) 相当于是给 \(d_l+1\)\(d_{r+1}-1\),等一下,是不是直接把负数最大值和正数最大值的绝对值加起来取个 \(max\) 就行了。

为什么题解没有这种做法服了。

话说为什么这题会被我归到差分约束呢?正睿题单里翻到的。

P4926 [1007] 倍杀测量者

一道好题,让我发现原来这种题目也能用二分和图论建模写。

首先是这个二分,观察到 \(T\) 越大越好(当然不能大于等于最小的 \(k_i\),否则 \(k_i-T\) 为负数),满足二分性质。

然后是如何建边,有两种不同的方法:

  • 在原条件下跑最小(大)乘积路
  • 在原条件下两边同时加个 \(lg\) 跑最短(长)路。

先看第一种,
我们将操作一不穿女装的限制转化为 \(s_A\ge(k-T)s_B\) 第二种为 \((k+T)s_A\ge s_B\),相当于在最小乘积图上我们建了 \(A\to B(w=k+T)\)\(A\to B(w=\dfrac{1}{k-T})\)
对于分数呢?我们一开始令源点 \(s_0\)\(dis_0=1\)(1 乘以任何数都是本身),那么有条件 \(s_A\ge Cs_0,s_A\le Cs_0\),就可以建 \(A\to 0\ (w=\dfrac{1}{C}),0\to A\ (w=C)\),然后就跑图就行了。

当然还有一个等效转化,我们上述限制如果均满足说明没有人需要穿女装,那么有人穿女装的条件自然就是有限制没有被满足。而限制没被满足意味着有一直变小和一直变大的乘积环,这里判断是否有点被加入队列 \(n+1\) 次以上即可。

复杂度O(卡一卡能过)。

哦对了,对于加对数的转化应该也差不多,反正想到这题的转化这题就不难了。

P.S. 貌似是因为 \(\dfrac{1}{a_i-k}\) 的精度损失太严重了,不能跑最短路。

\(Code\)

//最长路
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

以前模拟赛出过差不多的题,但是当时不会,想不到是建模,现在学了来看一下是不是能写出来吧。

假设我们有 \(i,j\),如果是 + 表示有关系 \(g_i\ge g_j+1\);如果是-,关系 \(g_i+1\le g_j\);如果是 = 关系,\(g_i\le g_j,g_i\ge g_j\);如果是 ?,因为什么都不知道,但是重量有限定,所以有关系 \(g_i\ge g_j-2,g_i\le g_j+2\)

有了这些关系之后,我们分别建两次图跑最大路和最短路找每个点的最大值和最小值,如果两者一样说明值确定,否则不确定。确定的值我们拿来枚举拿来用就行了(然而不是,只要最小值大于最大值就行了)。复杂度 \(O(\text{能过})\)

调着调着不知道为什么第二个点死活过不了。

16:26

\(5pts\ WA+TLE\ Code\)

#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;
}

样例是过了,这时发现其他点都过不了,然后我发现,我虽然能求出每个点的上下界,可是如果你直接粗略地拿具体值去比的话,只能得到必要条件不能得到全部条件,就比如人家可能确实存在 \(s_i+s_j=3\) 这个条件,可是我用了它们的下界算出来只有 \(2\),这样就错了。样例能过是因为这个样例它给的基本上都是上界等于下界即可以求出来的点。

这个错误也是我在题解里找的吧,然后发现我们要求的是 \(A+B</=/>i+j\)\(i,j\) 的对数,而用题目条件只能求出每对 \(g_i-g_j\) 的关系,然后我们发现把这些关系限制的上下限找出来也能找到我们想要的东西: \(A-i </=/>j-B\)。因为很数据很小,完全不需要用 SPFA,用 floyd 就行了。

P3084 [USACO13OPEN] Photo G

又是一个差分式的建模。\(sum_r-sum_{l-1}\le 1,sum_r-sum_{l-1}\ge 1\),还有限制诸如 \(0\le sum_i-sum_{i-1}\le 1\),最后求 \(sum_n-sum_0\)

18:52

发现果然不行,数据太大了,开 O2 都只能拿 70,想想怎么优化。

显然差分约束不是正解了,吗?
我们用类似于 dijkstra 的贪心,把队列换成优先队列,然后跑 SPFA,但是这个时候还是会 T 一个点,因为负环很难搞,怎么办呢?因为负环要跑很久,所以我们直接卡时即可通过本题,复杂度O(能过)。

考试的时候还是不要用优先队列了,有可能被卡飞起来,我们可以用一个双端队列的玄学方法代替:当当前加入值小于队首时前插,否则后插。

\(Code\)

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;
}

虽然是在打差分约束,但是还是想一想正解吧,毕竟这么卡时加玄学优化真的有点不人性。

假设我们一定要在一个地方放牛的话,我们尽可能保证之前的那只靠的足够地近,用 \(f_i\) 表示在 \(i\) 处一定有牛的最多牛数,那么有 \(f_i=f_{\max(j)}+1\),这个 \(j\) 就是 \(i\) 被包含的所有区间中左端点最小的那一个的左端点 \(-1\)

完了现在想不来,挖个坑吧。

P7624 [AHOI2021初中组] 地铁

(做为题解写的)

看到这个题目首先想到差分约束,但是环上问题比较麻烦。

我们首先想到断环成链,然后新加一些限制条件,然而发现这些限制条件都是多元的,无法用差分约束的形式表述。

我们重新回到原条件,直接在环上试试?

我们令 \(sum_i\) 表示前 \(i\) 段长度的前缀和,把边权化成点权就是我们把每条 \(i\to i+1\) 的边权赋给 \(i\)

那么如果我们有限制条件 \(s\to t\ge l\),反映到链上的限制就是:

  • \(s<t\),即 \(sum_t-sum_{s-1}\ge l\)
  • \(s>t\),相当于我们只有 \(t+1\sim s-1\) 的点权没有被取,那么限制就是 \(sum_n-(sum_{s-1}-sum_t)\ge l\),移项得 \(sum_{s-1}-sum_t\le sum_n-l\)

同理第二种限制条件。

而我们不难发现这个也是多元的,只不过很多时候会与这个 \(sum_n\) 有关,而这个 \(sum_n\) 不就是我们要求的吗?或许我们可以枚举这个 \(sum_n\),我们现在为了方便叫它 \(N\) 吧。

P3275 [SCOI2011] 糖果

11.9 15:32

这题的约束非常好写,写出来最长路的形式(因为要求最小值所以必须用最长路的形式)之后,跑了个卡时 SPFA 判正环并通过了样例,提交一看 \(0\) 分,超时超飞起导致全都跟跑负环差不多。

然后开始思考正解。发现每条边都只可能边为 \(0,1\),也就是说可以跑dij只要一个强连通分量上存在 \(1\) 就一定是正环,所以我们先把 \(0\) 的边单独拿出来跑缩点,然后再加边,如果拓扑图有环则无解(这次肯定是 \(1\) 环了)。然后你会发现如果只是判拓扑上有无环是错的,因为原来被缩的强连通图上也可能有还没有连的边但是指向同一个强连通分量内,也就是也有正环,所以这种写法两个都得判。

\(Code\)

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,只需要判一下是否存在强连通内部的点互相连 \(1\) 的即可。

\(Code2\)

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;
        }
    }
    //...
}
posted @ 2023-11-09 07:17  NBest  阅读(64)  评论(0)    收藏  举报