DAT4-5 圖論
最短路
性質
記\(dis[u]\)代表從源點走到u的最短路長度
1.貪心性:源點到任意一個點最短路上的每一步都是一個最短路
2.存在性:兩個點之間的最短路有可能不存在。(源點存在一個到達該點且經過一個負環的路徑/圖不連通)
3.三角形不等式:對於一條邊\(u\stackrel{w}{\to}v\),一定有\(dis[v]<=dis[u]+w\)
4.最短路圖:從源點出發,把所有\(dis[v]=dis[u]+w\)的邊建出來可以得到一張DAG。(不考慮0環)
演算法
鬆弛操作:對於一條邊,若當前\(dis[v]>dis[u]+w\),則置\(dis[v]=dis[u]+w\)
1.Bellman-Ford(SPFA)
\(O(nm)\)
2.Dijkstra
樸素\(O(n^2)\)
堆最佳化\(O((n+m)log(n+m))\)
只用於正權圖
3.Floyd
\(O(n^3)\)
4.Johnson
用Dijkstra跑全源最短路
解決負權邊:增加點權\(h[u]\),將邊的權值由\(w\)改為\(w+h[u]-h[v]\)
這樣不會影響最短路大小比較
\(h[u]\)滿足性質:\(w+h[u]-h[v]>=0\),即\(h[v]<=w+h[u]\)
建立一個超級源點(防止有點遍歷不到)跑Bellman-Ford,得到\(h[u]\)
\(O(nmlogm)\)
bool check(){
for(int i=1;i<=n;i++)h[i]=1e9;
queue<int>q;
q.push(0);
while(!q.empty()){
int u=q.front();
q.pop();
for(int j=0;j<a[u].size();j++){
int v=a[u][j].v,w=a[u][j].w;
if(h[v]>h[u]+w){
h[v]=h[u]+w;
q.push(v);
in[v]++;
if(in[v]>n+1)return 1;//判斷負環,加上超級源點共n+1個點
}
}
}
return 0;
}
void dijkstra(int s){
memset(vis,0,sizeof vis);
for(int i=1;i<=n;i++)d[i]=1e9;
d[s]=0;
priority_queue<node>q;
q.push(node{s,0});
while(!q.empty()){
node now=q.top();
q.pop();
if(vis[now.u])continue;
else vis[now.u]=1;
for(int j=0;j<a[now.u].size();j++){
int v=a[now.u][j].v,w=a[now.u][j].w;
if(d[v]>d[now.u]+w+h[now.u]-h[v]){
d[v]=d[now.u]+w+h[now.u]-h[v];
q.push(node{v,d[v]});
}
}
}
}
for(int i=1;i<=n;i++)a[0].push_back(edge{i,0});//建立超級源點
for(int i=1;i<=n;i++)dijkstra(i);//統計答案時特判路徑不存在的情況
問題
1.輸出方案
\(pre[i]\)記錄前驅節點
2.傳遞閉包
bitset最佳化\(O(\frac{n^3}{w})\)
3.選擇\(k\)條道路以\(c\)代價通行,求最短路
分層圖 \(O(kmlogm)\)
for(int i=1;i<=m;i++){//P4568 P4822
int u=read()+1,v=read()+1,w=read();
a[u].push_back(edge{v,w});
a[v].push_back(edge{u,w});
for(int j=1;j<=k;j++){
a[u+j*n].push_back(edge{v+j*n,w});
a[v+j*n].push_back(edge{u+j*n,w});
a[u+(j-1)*n].push_back(edge{v+j*n,0});
a[v+(j-1)*n].push_back(edge{u+j*n,0});
}
}
4.給出一個\(N\)個頂點\(M\)條邊的無向無權圖,頂點編號為\(1∼N\),求從頂點\(1\)開始,到其他各點的最短路條數
void bfs(){//P1144
...
for(int v:a[u]){
if(d[v]>d[u]+1){
d[v]=d[u]+1;
f[v]=f[u];
q.push(v);
}else if(d[v]==d[u]+1){
f[v]=(f[v]+f[u])%p;
}
}
...
}
P1462
二分點權,最短路判定
不開longlong見祖宗
P1119
理解Floyd本質
void floyd(){
init();
int now=1;
while(q--){
int u=read()+1,v=read()+1,tim=read();
while(t[now]<=tim&&now<=n){
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
f[i][j]=min(f[i][j],f[i][now]+f[now][j]);
now++;
}
ask(u,v);
}
}
P1772
動態規劃
列舉運輸路線的更換時間\(j\)
\(f[i]=min(f[j-1]+(i-j+1)\times{}dijkstra(j,i)+k)),j\in{}[1,i]\)
dijkstra時忽略不可到達的點
最終結果為\(f[n]-k\)
ll dijkstra(ll tl,ll tr){
...
for(int j=0;j<a[now.u].size();j++){
ll v=a[now.u][j].v,w=a[now.u][j].w;
if(check(v,tl,tr)){//判斷點是否可行
if(dist[v]>dist[now.u]+w){
dist[v]=dist[now.u]+w;
q.push(node{v,dist[v]});
}
}
}
...
}
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
f[i]=min(f[i],f[j-1]+1ll*(i-j+1)*dijkstra(j,i)+k);
P2371
擺
並查集
void init(){//P3367
for(int i=1;i<=n;i++)f[i]=i;
}
int find(int i){
if(f[i]==i)return i;
else return f[i]=find(f[i]);//路徑壓縮最佳化
}
void merge(int i,int j){//合併
f[find(i)]=find(j);
}
bool query(int i,int j){//查詢
return find(i)==find(j);
}
最佳化
1.路徑壓縮
2.按秩(啟發式)合併
void init(){
for(int i=1;i<=n;i++)f[i]=i,siz[i]=1;
}
void merge(int i,int j){
i=find(i),j=find(j);
if(i==j)return;
if(siz[i]>siz[j])swap(i,j);
f[i]=j;siz[j]+=siz[i];
}
任意一種最佳化單詞均攤複雜度為\(O(logn)\)
全部使用複雜度為\(O(\alpha(n))\)
問題
1.維護每個集合的點權和/maxn
帶權並查集
2.統計目前集合數量
for(int i=1;i<=n;i++)if(f[i]==i)ans++;
3.將某元素從集合A移動到集合B
修改根,維護資訊
4.利用並查集維護\(n\)個命題,有\(m\)個條件
條件為\(p\Leftrightarrow q\)或\(p\Leftrightarrow\neg{}q\)兩種
開正反兩個並查集
注意:必要的時候捨棄“路徑壓縮”操作。因為它破壞了並查集的樹結構。同時會造成一些複雜度錯誤。
P2024
//正反集
int f[150004];// 同類 敵人 食物
if(x>n||y>n||(x==y&&opt==2)){
ans++;
continue;
}
if(opt==1){
if(find(x+n)==find(y)||find(x+2*n)==find(y)){
ans++;
}else{
merge(x,y);
merge(x+n,y+n);
merge(x+2*n,y+2*n);
}
}else{
if(find(x)==find(y)||find(x+n)==find(y)||find(y+2*n)==find(x)){
ans++;
}else{
merge(x+2*n,y);
merge(x+n,y+2*n);
merge(x,y+n);
}
}
//帶權並查集
int find(int i){
if(f[i]!=i){
int tmp=f[i];
f[i]=find(f[i]);
d[i]=(d[i]+d[tmp])%3;
return f[i];
}else return i;
}
if(w==1)w=0;
if(u==v&&w==2){
ans++;
}else if(u>n||v>n){
ans++;
}else{
int fu=find(u),fv=find(v);
if(fu!=fv){
f[fu]=fv;
d[fu]=(w+d[v]-d[u]+3)%3;
}else if((d[u]-d[v]+3)%3!=w){
ans++;
}
}
生成樹
最小生成樹:邊權和最小的生成樹。
瓶頸生成樹:最大邊權最小的生成樹。
最小瓶頸路:兩個點之間最大邊權最小的簡單路徑。
性質:
最小生成樹是瓶頸生成樹的充分不必要條件,即一棵最小生成樹一定是一棵瓶頸生成樹,一棵瓶頸生成樹不一定是一棵最小生成樹。
最小生成樹上兩個點的路徑一定是一個最小瓶頸路。
演算法
1.Kruskal
\(O(mlogm)\)
2.Prim
\(O(mlogm)\)
問題
1.思考Kruskal的演算法原理,求結點\(1\)到結點\(n\)的最小瓶頸路的權值
節點\(1\)所在集合與節點\(n\)所在集合合併時,kruskal所選的邊權\(w\)即為結點\(1\)到結點\(n\)的最小瓶頸路的權值
2.並查集與Kruskal的演算法關係密切。在不使用路徑壓縮的情況下,整張圖的並查集會呈現一棵樹的樣貌。這棵樹和最小生成樹有什麼關係
瓶頸路
3.比較Prim和Dijkstra演算法的相似性。指出為何Prim可以處理負權邊和Dijkstra不行
4.獲得 “嚴格次小生成樹”
void dfs(int i,int fa){//P4180
d[i]=d[fa]+1;
for(int j=1;(1<<j)<=d[i];j++){
dp[i][j]=dp[dp[i][j-1]][j-1];
g[i][j]=max(g[dp[i][j-1]][j-1],g[i][j-1]);
//維護次大值
h[i][j]=max(h[dp[i][j-1]][j-1],h[i][j-1]);
if(g[dp[i][j-1]][j-1]!=g[i][j-1]){
h[i][j]=max(h[i][j],min(g[dp[i][j-1]][j-1],g[i][j-1]));
}
}
for(int j=0;j<a[i].size();j++){
int v=a[i][j].v,w=a[i][j].w;
if(v!=fa){
dp[v][0]=i;
g[v][0]=w;
dfs(v,i);
}
}
}
int ask(int i,int j,int lim){
int ret=0;
if(d[i]<d[j])swap(i,j);
for(int k=dis;k>=0;k--){
if(d[i]-(1<<k)>=d[j]){
if(g[i][k]==lim)ret=max(ret,h[i][k]);
else ret=max(ret,g[i][k]);
i=dp[i][k];
}
}
if(i==j)return ret;
for(int k=dis;k>=0;k--){
if(dp[i][k]!=dp[j][k]){
if(g[i][k]==lim)ret=max(ret,max(h[i][k],h[j][k]));
else ret=max(ret,max(g[i][k],g[j][k]));
i=dp[i][k];
j=dp[j][k];
}
}
if(g[i][0]!=lim)ret=max(ret,g[i][0]);
if(g[j][0]!=lim)ret=max(ret,g[j][0]);
return ret;
}
void solve(){
ans=inf;
for(int i=1;i<=m;i++){
if(vis[i]&&e[i].u!=e[i].v){//判斷自環
int tmp=ask(e[i].u,e[i].v,e[i].w);
ans=min(ans,sum+e[i].w-tmp);
}
}
cout<<ans;
}
init();
kruskal();//先求最小生成樹
dfs(1,0);//預處理最大值和次大值
solve();//用未選邊替換,更新答案
6.思考最小生成樹構建時的貪心性質,簡要解釋為何最小生成樹不止一個
P1967
求最大生成樹
在生成樹上倍增預處理最小值
lca查詢最小值
void dfs(int i,int fa){
d[i]=d[fa]+1;
col[i]=nums;
for(int j=1;(1<<j)<=d[i];j++){
dp[i][j]=dp[dp[i][j-1]][j-1];
maxn[i][j]=min(maxn[dp[i][j-1]][j-1],maxn[i][j-1]);
}
for(int j=0;j<a[i].size();j++){
int v=a[i][j].y,w=a[i][j].z;
if(v!=fa){
dp[v][0]=i;
maxn[v][0]=w;
dfs(v,i);
}
}
}
int ask(int i,int j){
int ret=0x3f3f3f3f;
if(d[i]<d[j])swap(i,j);
for(int k=dis;k>=0;k--){
if(d[i]-(1<<k)>=d[j]){
ret=min(ret,maxn[i][k]);
i=dp[i][k];
}
}
if(i==j)return ret;
for(int k=dis;k>=0;k--){
if(dp[i][k]!=dp[j][k]){
ret=min(ret,min(maxn[i][k],maxn[j][k]));
i=dp[i][k];
j=dp[j][k];
}
}
return min(ret,min(maxn[i][0],maxn[j][0]));
}
init();//預處理
kruskal();//求生成樹
for(int i=1;i<=n;i++){
if(!d[i]){
nums++;//圖不連通
dfs(i,0);
}
}
solve();//查詢
CF1513D
擺
樹上倍增
求lca
void init(int i,int fa){//P3379
d[i]=d[fa]+1;
f[i][0]=fa;
for(int j=1;(1<<j)<=d[i];j++){
f[i][j]=f[f[i][j-1]][j-1];
}
for(int j=0;j<a[i].size();j++){
int v=a[i][j];
if(v!=fa){
init(v,i);
}
}
}
int lca(int i,int j){
if(d[j]>d[i])swap(i,j);
for(int k=stp;k>=0;k--){//k>=0注意
if(d[i]-(1<<k)>=d[j]){
i=f[i][k];
}
}
if(i==j)return i;
for(int k=stp;k>=0;k--){//k>=0
if(f[i][k]!=f[j][k]){
i=f[i][k];
j=f[j][k];
}
}
return f[i][0];
}
init(s,0);
lca(i,j);
問題
1.求出 k 個點的樹上最近公共祖先
2.將k個點兩兩求樹上最近公共祖先
P4281
暴力去找
pair<int,int> ask(int i,int j){
int ret=0;
if(d[i]<d[j])swap(i,j);
for(int k=dis;k>=0;k--){
if(d[i]-(1<<k)>=d[j]){
ret+=dp[i][k];
i=f[i][k];
}
}
if(i==j)return make_pair(i,ret);
for(int k=dis;k>=0;k--){
if(f[i][k]!=f[j][k]){
ret+=dp[i][k]+dp[j][k];
i=f[i][k];
j=f[j][k];
}
}
return make_pair(f[i][0],ret+2);
}
pair<int,int> getp(int A,int B,int C){
pair<int,int> tmp=ask(B,C);
pair<int,int> ans=ask(tmp.first,A);
return make_pair(tmp.first,tmp.second+ans.second);
}
while(m--){
int x=read(),y=read(),z=read();
pair<int,int> t1=getp(y,z,x),t2=getp(x,z,y),t3=getp(z,y,x);
if(t1.second>t2.second)swap(t1,t2);
if(t1.second>t3.second)swap(t1,t3);
cout<<t1.first<<" "<<t1.second<<endl;
}
樹上差分
查詢
1.求樹上路徑經過點的個數
\(=dep[u]+dep[v]-dep[fa[lca]]-dep[lca]\)
2.求樹上路徑的邊權和
\(=dist[u]+dist[v]-2\times{}dist[lca]\)
修改
在\(p\rightarrow{}q\)路徑上的每個點點權\(+v\)
\(val[p]=val[p]+v\)
\(val[q]=val[q]+v\)
\(val[lca]=val[lca]-v\)
\(val[f[lca]]=val[f[lca]]-v\)
dfs自下向上查詢
P2680
運輸計劃為樹上一條路徑
使得運輸時間最長的計劃運輸時間最短,二分
對於大於\(mid\)的運輸計劃,找到這些運輸計劃共同經過的最大邊
判斷共同經過使用樹上差分
inline卡常
inline void dfs(int i,int fa){
for(int j=0;j<a[i].size();j++){
int v=a[i][j].v,w=a[i][j].w;
if(v!=fa){
dfs(v,i);
val[i]+=val[v];
}
}
for(int j=0;j<a[i].size();j++){//在更新完val[]後再判斷
int v=a[i][j].v,w=a[i][j].w;
if(v!=fa){
if(val[i]==cnt&&val[v]==cnt){//該邊被cnt條路徑經過
dmax=max(dmax,w);
}
}
}
}
inline bool check(ll x){
maxn=0;dmax=0;cnt=0;//最長運輸計劃耗時 可減少的最大時間 需要減少時間的邊數
memset(val,0,sizeof val);//注意
for(int i=1;i<=m;i++){
if(q[i].w>x){
int u=q[i].u,v=q[i].v;
maxn=max(maxn,q[i].w);cnt++;
int Lca=getsum(u,v).first;
//樹上差分
val[u]++;
val[v]++;
val[Lca]--;
val[f[Lca][0]]--;
}
}
dfs(1,0);
return maxn-dmax<=x;
}
ll l=0,r=maxn;
while(l<r){
ll mid=l+(r-l)/2;
if(check(mid))r=mid;
else l=mid+1;
}
hdu6053
難
有一棵樹,有\(n\)個節點,每個節點都有一個用整數表示的顏色型別,其中節點\(i\)的顏色是\(c_i\)
每兩個不同節點之間的路徑是唯一的,我們定義路徑的值為其中出現的不同顏色的數量
計算所有路徑的值的總和,該樹上總共有\(\frac{n(n-1)}{2}\)條路徑
定義\([P]\)若\(P\)為真則為\(1\),否則為\(0\)
即求
\({\textstyle \sum_{i=1}^{\frac{n(n-1)}{2}}}{\textstyle\sum_{j=1}^{colnums}[c_j出現次數>=1]}\)
\(={\textstyle \sum_{j=1}^{colnums}{\textstyle\sum_{i=1}^{\frac{n(n-1)}{2}}}[c_j出現次數>=1]}\)
\(=colnums\times\frac{n(n-1)}{2}-{\textstyle \sum_{j=1}^{colnums}{\textstyle\sum_{i=1}^{\frac{n(n-1)}{2}}}[c_j出現次數=0]}\)
// 以1節點為根
ll fi(ll i){//i為連通塊大小,返回連通塊邊數
return i*(i-1)/2;
}
void dfs(int i,int fa){
//sum[i]表示當前以i顏色節點為根的子樹大小
ll inssum=0;//記錄增長
siz[i]=1;//siz[i]表示以節點i為根的子樹大小
for(int j=0;j<a[i].size();j++){
int v=a[i][j];
if(v!=fa){
ll pre=sum[c[i]];//記錄過去大小
dfs(v,i);//遞迴子樹
siz[i]+=siz[v];//更新
ll ins=sum[c[i]]-pre;//遞迴子樹之後,sum[c[i]]的增長值,也就是子樹內以i顏色節點為根的子樹大小
sub+=fi(siz[v]-ins);//子樹大小-ins即為顏色c[i]不出現的連通塊大小
inssum+=ins;//記錄
}
}
sum[c[i]]+=siz[i]-inssum;//加上以當前節點為根的子樹內的其它節點個數
}
void solve(){
dfs(1,0);//統計sub,sub為每種顏色未出現邊數之和
for(int i=1;i<=n;i++)
cnt+=(csum[i]>0);//統計顏色種類
//統計每種顏色最淺節點到根之間的貢獻
vis[c[1]]=1;
for(int i=1;i<=n;i++){
if(!vis[c[i]]){
sub+=fi(n-sum[c[i]]);
vis[c[i]]=1;
}
}
ans=cnt*fi(n)-sub;
++cas;
printf("Case #%lld: %lld\n",cas,ans);
}
Clear();//注意
for()csum[c[i]]++;//統計每種顏色出現次數
solve();
P4768
P1600
P3953
P1084
擺