建國中學 陳仲肯
「meow」by蔡俊則
MST Minimum Spanning Tree
對於所有連通圖,點數與原圖相同、邊數最少的連通子圖
一定是一棵樹,稱為生成樹
邊權總和最小的生成樹即稱為最小生成樹
一個連通圖內兩個未完成的最小生成樹
用最短的邊連接一定最好
每次找當前權重最小的邊,
看端點是否在兩個不同的最小生成樹中,
是的話就把兩棵樹合併起來。
如何合併?
還記得並查集嗎?
把所有邊排序,所有邊看過一次並查集
\(O(E log E)\)
struct E{int u,v,w;};
bool cmp(E a,E b){return a.w<b.w;}
E edge[N];
int Kruskal(){
sort(edge, edge+m, cmp);
int ans=0;
for(int i=0;i<m;i++){
if(fa(edge[i].u)!=fa(edge[i].v)){
ans+=edge[i].w;
join(edge[i].u, edge[i].v);
}
}
return ans;
}
跟Dijkstra類似
把「與原點距離」改成「與樹距離」
pq的話跟dijkstra滿像的,每條邊push一次,對每個點pop一次
struct E{int v,w;};
struct cmp{bool operator()(E a,E b){return a.w>b.w;}};
priority_queue<E,vector<E>,cmp> pq;
pq.push({1,0});
int ans=0;
dis[1]=0;//到樹距離
while(!pq.empty()){
E cur=pq.top();pq.pop();
if(vis[cur.v])continue;
vis[cur.v]=1;
ans+=cur.w;
for(auto [v,w]:G[cur.v]){
if(!vis[v]&&dis[v]>w){
dis[v]=w;
pq.push({v,w});
}
}
}
我在寫Dijkstra時被Prim混淆,以為是存到樹距離
裸題
每條邊有a,b兩種權重,求生成樹中\(\frac{\sum{a}}{\sum{b}}\)最小的
好像不能直接做?
二分搜可以湊出來的最小比率!
如果我希望有一棵生成樹\(\frac{\sum{a}}{\sum{b}}<r\)
則\(\sum{a}-r\sum{b}<0\)
把每個邊的邊權設為\(a-rb\)
找最小生成樹,如果總和<0就可以
做\(O(logC)\)次MST
C是\(值域\div精度\)
\(O((E+V)logElogC)\)
或\(O(ElogElogC)\)
for無向圖
讀作:塔樣
DFS TREEEEEEE
無向圖的DFS TREEEEEEE
Tarjan的想法
對於一個不是根點的u,有任何一個小孩子樹裡所有back edge的終點都在u的子樹裡,則u是關節點
對於根點,如果dfs tree 上小孩個數>=2
就是關節點
e.g. 賴賴跟Wiwi有兩個小孩(小賴跟小Wi)
u
u的祖先
\(low_i:=i\)的子樹中的back edge的\(dfs\)序最小值
int cnt=0;
int low[N],in[N];
vector<int> G[N];
void dfstreeeeee(int u,int fa=-1){
low[u]=in[u]=++cnt;
int child=0;
for(int v:G[u]){
if(v==fa)continue;
if(in[v])low[u]=min(low[u],in[v]);//u到v是back edge
else{
dfstreeeeee(v,u);
if(fa!=-1&&low[v]>=in[u])IS_CUTPOINT(u);//u把v和fa分開
++child;low[u]=min(low[u],low[v]);
}
}
if(fa==-1&&child>1)IS_CUTPOINT(u);
}
for無向圖
橋?
橋牌?
拔掉會使圖不連通的邊
讀作:塔樣
DFS TREEEEEEE
Back edge不會是橋
看Tree edge就好
Tarjan的想法
對於一個點u如果他兒子v的子樹裡所有 back edge的終點都在子樹裡
則u--v是bridge
u
u的兒子
\(low_i:=i\)的子樹中的back edge的\(dfs\)序最小值
int cnt=0;
int low[N],in[N];
vector<int> G[N];
void dfstreeeeee(int u,int fa=-1){
low[u]=in[u]=++cnt;
int child=0;
for(int v:G[u]){
if(v==fa)continue;
if(in[v])low[u]=min(low[u],in[v]);//u到v是back edge
else{
dfstreeeeee(v,u);
low[u]=min(low[u],low[v]);
if(low[v]==in[v])IS_BRIDGE(u,v);
}
}
}
這兩個名詞都只適用於有向圖
讀作:塔樣
DFS TREEEEEEE
如果一個點的子樹經過back edge最高都只能到他自己則他是SCC的頭
開一個stack,存現在有可能在SCC的點
DFS時,看完相鄰的點,如果low[u]==in[u],則stack中直到自己的點都在同一個SCC,pop掉
每個點只會在一個SCC中,所以在最高點處理沒問題
void dfs(int u){
low[u]=in[u]=cnt++;
stk.push(u);
for(int v:G[u]){
if(!vis[v]){
dfs(v);
low[u]=min(low[u],low[v]);
}else if(!scc[v]){
low[u]=min(low[u],in[v]);
}
}
if(low[u]==in[u]){
int cur;
do{
cur=stk.top();stk.pop();
scc[cur]=scctot;
}while(cur!=u);
++scctot;
}
}
把每個SCC當作一個點,邊照連
PROOF:有環的話就應該要縮起來
需要一點巧思
多設一個點0,代表任何點
有傳送站的可以到0
0可以到任何點
然後就跟上一題一樣了
另一個找SCC的演算法
做一次DFS,紀錄離開節點的順序
從後離開的開始對反向圖再DFS一次,能到的點就在同一個SCC
考慮u,v間的路徑
void dfs(int u){
if(vis[u])return;
vis[u]=1;
for(int v:G[u])dfs(u);
out.push(u)
}
void bfs(int u){
if(scc[u])return;
scc[u]=cnt;
for(int v:R[u])bfs(v);
}
好像其實超好寫
但Tarjan常數比較小?
#include <bits/stdc++.h>
#define pb push_back
using namespace std;
int cnt=0;
const int N=1e5+10;
bool vis[N];
int scc[N];
vector<int> G[N],R[N];
stack<int> out;
void bfs(int u){
if(vis[u])return;
vis[u]=1;
for(int v:R[u])bfs(v);
out.push(u);
}
void dfs(int u){
if(scc[u])return;
scc[u]=cnt;
for(int v:G[u])dfs(v);
}
int main(){
int n,m,a,b,u;
cin>>n>>m;
while(m--){
cin>>a>>b;
G[a].pb(b);
R[b].pb(a);
}
for(int i=1;i<=n;i++)bfs(i);
while(!out.empty()){
u=out.top();
out.pop();
if(scc[u])continue;
cnt++;
dfs(u);
}
cout<<cnt<<"\n";
for(int i=1;i<=n;i++){
cout<<scc[i]<<" ";
}
return 0;
}
by謝一
#include <bits/stdc++.h>
using namespace std;
int cnt, sccn, scc[100010];
bool vis[100010], in[100010];
pair<int, int> past[100010];
vector<int> road[100010];
vector<int> backway[100010];
void dfsb(int now){
past[now].second = now;
vis[now] = true;
for(int i = 0; i < backway[now].size(); i++){
if(!vis[backway[now][i]]) dfsb(backway[now][i]);
}
past[now].first = cnt;
cnt++;
}
void dfs(int now){
vis[now] = true;
scc[now] = sccn;
for(int i = 0; i < road[now].size(); i++){
if(!vis[road[now][i]]){
dfs(road[now][i]);
}
}
}
int main(){
int n, m, a, b, ans;
cin >> n >> m;
past[0] = {0, 0};
ans = 0;
for(int i = 1; i <= n; i++){
road[i].clear();
backway[i].clear();
vis[i] = false;
}
for(int i = 0; i < m; i++){
cin >> a >> b;
road[a].push_back(b);
backway[b].push_back(a);
}
cnt = 1;
for(int i = 1; i <= n; i++){
if(!vis[i]) dfsb(i);
}
sort(past + 1, past + n + 1);
memset(vis, false, n + 1);
sccn = 0;
for(int i = n; i > 0; i--){
if(!vis[past[i].second]){
dfs(past[i].second);
sccn++;
}
}
memset(in, true, sccn);
for(int i = 1; i <= n; i++){
for(int j = 0; j < road[i].size(); j++){
if(scc[road[i][j]] != scc[i]) in[scc[road[i][j]]] = false;
}
}
for(int i = 0; i < sccn; i++){
if(in[i]) ans++;
}
cout << ans << "\n";
return 0;
}
不存在關節點的連通分量
讀作:塔樣
DFS TREEEEEEE
一個點可能在多個BCC中
但一條邊一定會且只會在一個BCC中
點雙連通分量可以把邊分群!
用找關節點的方法+紀錄SCC的方法
找到時把stack裡的pop掉
記得考慮關節點
int low[N],tin[N],id[N],t,cnt;
vector<int> bcc[N],G[N];
stack<int> s;
void dfs(int u,int p=0){
low[u]=tin[u]=++t;
s.push(u);
for(int v:G[u])if(v!=p){
if(tin[v])low[u]=min(low[u],tin[v]);
else{
dfs(v,u);
if(low[v]>=tin[u]){
cnt++;
int k;
do{
k=s.top();s.pop();
bcc[k].pb(cnt);
}while(k!=v);
bcc[u].pb(cnt);
}
low[u]=min(low[u],low[v]);
}
}
}
bool iscut[N];
vector<int> T[N];
void build(){
dfs(1);
for(int i=1;i<=n;i++){
if(bcc[i].size()>1){
id[i]=++cnt;
iscut[id[i]]=true;
for(int j:bcc[i]){
T[id[i]].PB(j);
T[j].PB(id[i]);
}
}else{
id[i]=bcc[i][0];
}
}
}
把每個BCC縮點,中間用關節點連起來
對於有些問題,於把圖的問題變成樹的問題會大大簡化問題
我弱,對不起
不存在橋的連通分量
讀作:塔樣
DFS TREEEEEEE
邊不一定都在BCC中
但一條點一定會且只會在一個BCC中
點BCC是把邊分群
邊BCC是把點分群
void dfs(int u,int be=-1){//be: edge from last vertex
++cnt;
vis[u]=low[u]=cnt;
stk.push(u);
for(int e:G[u]){
if(e==be)continue;
int v=E[e];
if(!vis[v]){
dfs(v,e^1);
low[u]=min(low[u],low[v]);
if(low[v]>vis[u]){//u-v is bridge
++bcctot;
int cur;
do{
cur=stk.top();stk.pop();
bcc[bcctot].pb(cur);
}while(cur!=v);
}
}
else if(vis[v]<vis[u])low[u]=min(low[u],low[v]);
}
}
扣
一定會是樹(沒有環)
對於有些問題,於把圖的問題變成樹的問題會大大簡化問題
我弱,對不起
不知道講不講得到這裡
#include <bits/stdc++.h>
#define pb push_back
using namespace std;
const int N=1e5+10;
int cnt=0;
bool ans[N],vis[2*N];
int scc[2*N],in[2*N];
vector<int> G[2*N],R[2*N],S[2*N],C[2*N];
stack<int> out,ord;
void bfs(int u){
if(vis[u])return;
vis[u]=1;
for(int v:R[u])bfs(v);
out.push(u);
}
void dfs(int u){
if(scc[u])return;
scc[u]=cnt;
for(int v:G[u])dfs(v);
}
void topological_sort(int n){
int u;
bool ok;
queue<int> Q;
for(int i=1;i<=cnt;i++){
if(!in[i])Q.push(i);
}
while(!Q.empty()){
u=Q.front();
Q.pop();
ord.push(u);
for(int v:S[u]){
in[v]--;
if(!in[v])Q.push(v);
}
}
while(!ord.empty()){
u=ord.top();
ord.pop();
ok=1;
for(int v:C[u]){
if(vis[v/2])ok=0;
}
if(!ok)continue;
for(int v:C[u]){
ans[v/2]=v&1;
vis[v/2]=1;
}
}
}
signed main(){
int n,m,a,b,u;
char wa,wb;
cin>>m>>n;
while(m--){
cin>>wa>>a>>wb>>b;
a<<=1,b<<=1;
if(wa=='+')a++;
if(wb=='+')b++;
G[a^1].pb(b);
G[b^1].pb(a);
R[b].pb(a^1);
R[a].pb(b^1);
}
for(int i=2;i<=2*n+1;i++){
bfs(i);
}
while(!out.empty()){
u=out.top();
out.pop();
if(!scc[u])cnt++;
dfs(u);
}
for(int i=1;i<=n;i++){
if(scc[2*i]==scc[2*i+1]){
cout<<"IMPOSSIBLE";
return 0;
}
}
for(int i=2;i<=2*n+1;i++){
C[scc[i]].pb(i);
for(int v:G[i]){
if(scc[v]==scc[i])continue;
S[scc[i]].pb(scc[v]);
in[scc[v]]++;
}
vis[i/2]=0;
}
topological_sort(n);
for(int i=1;i<=n;i++){
cout<<(ans[i]?"+":"-");
}
return 0;
}
課後問題: 想一下3-SAT怎麼做
劉澈給的題目都不是圖論的...