24暑假集訓day5上午

Yantai_YZY發表於2024-08-03

圖論

差分約束

\(𝑛\) 個整數變數 \(𝑥_1∼𝑥_𝑛\)

給定一些形如 \(𝑥_𝑖+𝑐≥𝑥_𝑗\) 的限制。問有沒有可行解,如有輸出方案。

例如 \(𝑥_1−1≥𝑥_2,𝑥_2≥𝑥_3,𝑥_3≥𝑥_1\) 就無解。

在單源最短路問題中,如果存在一條 \(𝑖→𝑗\) 長為 \(𝑤\) 的邊,在計算 \(1\) 號點到每個點的最短路後,一定有 \(𝑑𝑖𝑠[𝑖]+𝑤≥𝑑𝑖𝑠[𝑗]\)

所以對於每個 \(𝑥_𝑖+𝑐≥𝑥_𝑗\) 的限制,可以連邊 \((𝑖,𝑗,𝑐)\)

如果圖中存在負環,則無解。假如負環節點編號為 \(1,2,3,…,𝑘\),那麼 \(𝑥_1≥𝑥_2−𝑐_{12}≥𝑥_3−𝑐_{12}−𝑐_{23}≥…≥𝑥_1−𝑐_{12}−𝑐_{23}−…−𝑐_{𝑘1}\),而 \(𝑐_{12}+𝑐_{23}+…+𝑐_{𝑘1}<0\),所以無解。

如果圖中沒有負環,則有解。跑完最短路後,令 \(𝑥_𝑖=𝑑𝑖𝑠[𝑖]\) 即可。

如果有 \(𝑥_𝑖+𝑐=𝑥_𝑗\) 的限制,就拆成 \(𝑥_𝑖+𝑐≥𝑥_𝑗\)\(𝑥_𝑗−𝑐≥𝑥_𝑖\)。所以可以連邊 \((𝑖,𝑗,𝑐),(𝑗,𝑖,−𝑐)\)

圖不連通時,各個連通塊可以獨立考慮。

差分約束求值

注意到我們可以給每個 \(𝑥_𝑖\) 都加上一個 \(Δ\),並不影響每個限制 \(𝑥_𝑖+𝑐≥𝑥_𝑗\)。所以無法“求出一組合法解,使得 \(𝑥_𝑝\) 最大 / 最小”。

但是可以“求出一組合法解,使得 \(𝑥_𝑝−𝑥_𝑞\) 最大 / 最小”。

由於 \(𝑥_𝑝−𝑥_𝑞\) 最小 \(⇔𝑥_𝑞−𝑥_𝑝\) 最大,所以我們只考慮讓 \(𝑥_𝑝−𝑥_𝑞\) 最大。

由於 \(𝑥_𝑝−𝑥_𝑞≤𝑑𝑖𝑠(𝑞,𝑝)\),所以 \(𝑥_𝑝−𝑥_𝑞\) 再大也不能比 \(𝑑𝑖𝑠(𝑞,𝑝)\) 大。而從 \(𝑞\) 點跑一遍最短路後,\(𝑥_𝑝−𝑥_𝑞\) 正好取到 \(𝑑𝑖𝑠(𝑞,𝑝)\),所以這就是合法的最大值。此時的 \(𝑑𝑖𝑠\) 陣列\(𝑑𝑖𝑠[𝑖]=𝑑𝑖𝑠(𝑞,𝑖)\)就是滿足 \(𝑥_𝑝−𝑥_𝑞\) 最大時合法的解。

如果有 \(𝑥_𝑖≥𝑤\) 的限制,就連邊 \((𝑖,\)起點\(,−𝑤)\),因為 \(𝑑𝑖𝑠[\)起點\(]=0\),所以 \(𝑥_𝑖≥𝑤\) 相當於 \(𝑥_𝑖−𝑤≥𝑥_{起點}=0\)

小K的農場

思路:

\(𝑦_𝑗\) 為農場 \(𝑗\) 的作物單位數。

農場 \(𝑏\) 比農場 \(𝑐\) 至少多種了 ¥𝑑$ 的作物:\(𝑦_𝑏 − 𝑑 ≥ 𝑦_𝑐\),連邊 \(𝑏, 𝑐, −𝑑\)

農場 \(𝑏\) 比農場 \(𝑐\) 至多多種了 \(𝑑\) 的作物:\(𝑦_𝑐 + 𝑑 ≥ 𝑦_𝑏\),連邊 \(𝑐, 𝑏, 𝑑\)

農場 \(𝑏\) 與農場 \(𝑐\) 種植作物一樣多:\(𝑦_𝑏 = 𝑦_𝑐\),連邊 \(𝑏, 𝑐, 0\),\(𝑐, 𝑏, 0\)

使用 SPFA 判斷圖中是否有負環。有負環則無解,無負環則有解。

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
#include<set>
#include<unordered_map>
#include<bitset>
#include<climits>
#include<cassert>
using namespace std;
const int MAXN=100005;
int n,m,s,dis[MAXN];
bool inqueue[MAXN];
queue<int> Q;
int flag[MAXN];
vector<pair<int,int> > edges[MAXN];
int main(){
	int n,m;
	cin >> n >> m ;
	memset(dis,0x3f,sizeof(dis));
	for(int i=0,u,v,w;i<m;i++){
		int op,a,b,c;
		cin>>op;
		if(op==1){
			cin >> a >> b >> c;
			edges[a].emplace_back(b,-c);
		}else if(op==2){
			cin >> a >> b >> c;
			edges[b].emplace_back(a,c);
		}else{
			cin >> a >> b;
			edges[a].emplace_back(b,0);
			edges[b].emplace_back(a,0);
		}
	}
	for(int i=1;i<=n;i++){
	    edges[0].emplace_back(i,0);
	}
	dis[s]=0;
	Q.push(s);
	inqueue[s]=true;
	while(!Q.empty()){
		int x=Q.front();
		Q.pop();
		inqueue[x]=false;
		for(auto edge:edges[x]){
			if(dis[edge.first]<=dis[x]+edge.second)continue;
			dis[edge.first]=dis[x]+edge.second;
			if(!inqueue[edge.first]){
				Q.push(edge.first);
				flag[edge.first]=flag[x]+1;
				if(flag[edge.first]>n){
					cout<<"No\n";
					return 0;
				}
				inqueue[edge.first]=true;
			}
		}
	}
	cout<<"Yes\n";
	return 0;
}

拓撲排序

對於一個有向無環圖,為每個節點 \(𝑗\) 分配一個順序 \(ord_𝑗\),使得對於任意有向邊 \(𝑣 → 𝑤\),都有 \(ord_𝑣 < ord_𝑤\)

在有環圖上無法做到,假設環為 \(a_1 , a_2 , … , a_𝑛\),則需要 \(ord{a_1} <ord{a_2} < ⋯ < ord{a_𝑚} < ord{a_1}\),矛盾。

最開始的節點一定不能有入度。所以我們可以每次任選一個沒有入度的節點,將其拓撲序置為 \(1\)。然後刪除此節點,遞迴處理剩下的圖(繼續找到沒有入度的點,將其拓撲序置為 \(2 ,…\))。

由於刪完節點後,圖仍然是 DAG,所以存在合法拓撲序。

換句話說,任選一個沒有入度的節點都是合法的。

用一個佇列維護所有入度 \(=0\) 的點。

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
#include<set>
#include<unordered_map>
#include<bitset>
#include<climits>
#include<cassert>
#define int long long
using namespace std;
const int N=100005;

inline int read(){
	int x=0,f=1;
	char ch=getchar();
	while (ch<'0'||ch>'9'){
		if (ch=='-') f=-1;
		ch=getchar();
	}
	while (ch>='0'&&ch<='9'){
		x=x*10+ch-48;
		ch=getchar();
	}
	return x*f;
}
int ord[N], inDegree[N];
int n, m;
queue<int> Q;
vector<int> nextPoints[N];
signed main(){
    cin >> n >> m;
    for(int i=0,u, v; i< m; i++){
        cin >> u >> v;
        nextPoints[u].push_back(v);
        inDegree[v]++;
    }
    for(int i=0;i<n; i++){
        if(inDegree[i]==0){
            Q.push(i);
        }
    }
    int cnt = 0;
    while(!Q.empty()){
        int x=Q.front();
        Q.pop();
        ord[x] = ++cnt;
        for(auto y: nextPoints[x]){
            inDegree[y]--;
            if(inDegree[y]==0){
                Q.push(y);
            }
        }
    }
    return 0;
}

如果原圖中存在環,那麼這個環以及其能到達的所有點,都不會被刪除。

所以可以使用拓撲排序判斷有向圖是否是 DAG。只需要在結束時判斷 \(𝑐𝑛𝑡==𝑛?\) 即可。


兩個相似的問題

要求拓撲序靠前的編號儘量小。即最小化 \(𝑜𝑟𝑑^{−1}\) 的字典序。

要求編號小的拓撲序儘量靠前。即最小化 \(𝑜𝑟𝑑\) 的字典序。

其中 \(𝑜𝑟𝑑^(−1)\) 的意思是 \(𝑜𝑟𝑑^{−1} [𝑜𝑟𝑑[𝑖]]=𝑖\),即 \(𝑜𝑟𝑑^{−1}\) \([𝑖]\) 表示拓撲序第 \(𝑖\) 位的節點編號。

拓撲序靠前的編號儘量小

普通的拓撲排序每次任選一個 inDegree=0 的節點。現在我們只需要每次取編號最小的節點即可。

用優先佇列替換佇列維護 inDegree=0 的節點,每次取出編號最小的節點。

編號小的拓撲序儘量靠前

直接做是不可行的,我們不知道刪掉哪個點能最快到達 \(1\) 號點。

但是我們可以讓“編號小的拓撲序儘量靠後”:儘量拖延刪除 1 號點的時間,直到不得已再刪(此時佇列中只有 \(1\) 號點)。在此基礎上,儘量拖延刪除 \(2\) 號點,… 也就是每次刪除編號最大的點。

然後反向建圖即可。反向的拓撲序靠後就是正向的拓撲序靠前。


拓撲序計數

給定一張有向無環圖,求其合法拓撲序個數。

\(𝑛≤20,𝑚≤\frac{𝑛(𝑛−1)}{2}\)

\(𝑓(𝑆)\) 表示 \(𝑆\) 誘導子圖的拓撲序個數。

轉移時,列舉 \(𝑆\) 中拓撲序最靠前的節點 𝑖 :

\[𝑓(𝑆)=∑_{𝑖∈𝑆,ind_𝑖=0}𝑓(𝑆−\{𝑖\}) \]

其中 ind_𝑖=0 表示 \(𝑖\)\(𝑆\) 中沒有入度(而非整個圖中)。


次小生成樹問題

求次小生成樹。(可能與最小生成樹邊權和相等)

\(𝑛≤1000\)

第三小?


經過 1 號點的最小環

給定一個有向圖,無重邊無自環,求經過 \(1\) 號點的最小環。

邊權非負,\(𝑛,𝑚≤10^5\)