圖論
差分約束
有 \(𝑛\) 個整數變數 \(𝑥_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
表示 \(𝑖\) 在 \(𝑆\) 中沒有入度(而非整個圖中)。
次小生成樹問題
求次小生成樹。(可能與最小生成樹邊權和相等)
\(𝑛≤1000\)
第三小?
經過 1 號點的最小環
給定一個有向圖,無重邊無自環,求經過 \(1\) 號點的最小環。
邊權非負,\(𝑛,𝑚≤10^5\)。