注:dijisktra无法求有负权的图
【模板】单源最短路
每次从已经访问的节点中选出 d i s dis dis 最小的节点然后松弛未访问的
那么朴素算法显然需要 O ( n 2 ) O(n^2) O(n2) 的总时间复杂度
从已经访问的节点中选出 d i s dis dis 最小的节点这一操作可以用 堆 ( p r i o r i t y priority priority_ q u e u e queue queue ) 优化
到 l o g n log n logn (这一操作的复杂度)
//堆优化djs
struct Node{
int to,w;
bool operator < (const Node &x) const{
return x.w<w;
}
};
void dijisktra()
{
dis[s]=0;
q.push(Node{s,0});
while(!q.empty())
{
Node hd=q.top();q.pop();
int u=hd.u;
if(vis[u]) continue;vis[u]=1;
for(int i=0;i<G[u].size();i++)
{
int v=G[u][i].to;
if(dis[v]>hd.w+G[u][i].w)
{
dis[v]=hd.w+G[u][i].w;
if(!vis[v]) q.push(Node{v,dis[v]});
}
}
}
}
[USACO06NOV]Roadblocks G (次短路)
求次短路无非在松弛时多一个比较
若结果比最短路小,则更新最短路,并将原来最短路变为次短路
若结果大于最短路小于次短路,则更新次短路,不更新最短路
//堆优化djs求次短路
void dijisktra()
{
dis1[s]=0;
q.push(Node{s,0});
while(!q.empty())
{
Node hd=q.top();q.pop();
int u=hd.u,dis=hd.w;
for(int i=0;i<G[u].size();i++)
{
int v=G[u][i].to;
if(dis1[v]>dis+G[u][i].w)
{
dis2[v]=dis1[v];
dis1[v]=dis+G[u][i].w;
q.push(Node{v,dis1[v]});
}
else if(dis2[v]>dis+G[u][i].w && dis1[v]<dis+G[u][i].w)
{
dis2[v]=dis+G[u][i].w;
q.push(Node{v,dis2[v]});
}
}
}
}
注意,这里不加 v i s vis vis 数组是因为次短路有可能是最短路中之前某个已访问节点重复走经过该点的某一条边而形成的次短路
比如,
1 1 1 到 3 3 3 的最短路为 1 − 2 − 3 1-2-3 1−2−3
而次短路为 1 − 2 − 1 − 2 − 3 1-2-1-2-3 1−2−1−2−3
S P F A SPFA SPFA 是队列优化的 B e l l m a n Bellman Bellman F o r d Ford Ford
因其时间复杂度不稳定而经常被卡常
void spfa(int s)
{
q.push(s);D[s]=0;
inque[s]=1;
while(!q.empty())
{
int hd=q.front(); q.pop();
inque[hd]=0;
for(int i=0;i<G[hd].size();i++)
{
int v=G[hd][i].to;
if(dis[v]>dis[hd]+G[hd][i].w)
{
dis[v]=dis[hd]+G[hd][i].w
if(!inque[v]) q.push(v); inque[v]=1;
}
}
}
}
S P F A SPFA SPFA 是队列优化的 b e l l m a n − f o r d bellman-ford bellman−ford
思路是对于每个已访问的点,枚举所有未访问的点进行松弛
while(!q.empty())
{
int hd=q.front(); inque[hd]=1;
for(int i=0;i<G[hd].size();i++)
{
int v=G[hd][i].to;
if(dis[v]>dis[hd]+G[hd][i].w)
{
dis[v]=dis[hd]+G[hd][i].w;
if(!inque[v]) q.push(v);
}
}
q.pop(); inque[hd]=0;
}
但是, S P F A SPFA SPFA 和 B e l l m a n Bellman Bellman F o r d Ford Ford 可以求负环
void dfs(int u)
{
inque[u]=1;
for(int i=0;i<G[u].size();i++)
{
int v=G[u][i].to;
if(dis[v]>dis[u]+G[u][i].w)
{
if(inque[v]==1) flag=true;
dis[v]=dis[u]+G[u][i].w;
dfs(v);
}
}
inque[u]=false;
}
核心就在这一句
if(inque[v]==1) flag=true;
意思是说,我当前这个点 u u u 就是从 v v v 这个点跑上来的,但现在我又要松弛 v v v ,说明肯定形成了一个环,而因为是最短路形成环且又要被松弛一遍说明肯定形成了负环
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
模板相信大家都会,而 k k k 为何放在最外层呢?
实际上是动态规划的思想
f [ i ] [ j ] [ k ] f[i][j][k] f[i][j][k] 表示以 i i i 为起点, j j j 为终点,经过前 k k k 个结点的最短路
那么有
f ( i , j , k ) = m i n f(i,j,k)=min f(i,j,k)=min ( f ( i , j , k ) f(i,j,k) f(i,j,k) , f ( i , k , k − 1 ) + f ( k , j , k − 1 ) f(i,k,k-1)+f(k,j,k-1) f(i,k,k−1)+f(k,j,k−1) )
然后用滚动数组将 k k k 滚掉
iai 确定排名
不等式的传递性, f [ x ] [ y ] = t r u e f[x][y]=true f[x][y]=true 指的是 x > y x>y x>y
那么, f [ i ] [ j ] ∣ = ( f [ i ] [ k ] f[i][j] | = (f[i][k] f[i][j]∣=(f[i][k] & f [ k ] [ j ] ) f[k][j]) f[k][j])
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v;
cin>>u>>v;
f[u][v]=1;
}
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
f[i][j] |= f[i][k] & f[k][j];
int ans=0;
for(int i=1;i<=n;i++)
if(check(i)) ans++;
【模板】最近公共祖先
f a [ u ] [ k ] fa[u][k] fa[u][k] 表示 u u u 结点的 2 k 2^k 2k 级祖先
先跑一遍 d f s dfs dfs 求出各节点的深度 d e p t h depth depth 以及各节点的各级祖先
void dfs(int u,int fa)
{
p[u][0]=fa; dep[u]=dep[fa]+1;
for(int i=1;i<=lg[dep[u]];i++)
p[u][i]=p[p[u][i-1]][i-1];
for(int i=0;i<G[u].size();i++) {
int v=G[u][i];
if(v==fa) continue;
dfs(v,u);
}
}
然后,求 x , y x,y x,y 的 l c a lca lca
先让深度更深的点倍增跳到同一深度,再一起往上跳,让他们一直跳到 l c a lca lca 下面的两个点(不让他们跳到相同的点即可)
void lca(int x,int y)
{
if(dep[x]<dep[y]) swap(x,y);
while(dep[x]>dep[y]) x=p[x][lg[dep[x]-dep[y]]];
if(x==y) {cout<<x<<endl;return ;}
for(int k=lg[dep[x]];k>=0;k--)
if(p[x][k]!=p[y][k])
x=p[x][k],y=p[y][k];
cout<<p[x][0]<<endl;
}
生成树问题大致分为两种做法 K r u s k a l Kruskal Kruskal 和 P r i m Prim Prim,一般都用 K r u s k a l Kruskal Kruskal
将所有边排序,每次取最短且合法的边加入最小生成树
void Kruskal()
{
int sum=0,cnt=0;
sort(e+1,e+m+1);
for(int i=1;i<=m;i++)
{
if(cnt==n-1) break;
int u=e[i].u,v=e[i].v;
int root_u=find(u),root_v=find(v);
if(root_u==root_v) continue;
fa[root_u]=root_v;
sum+=e[i].w; cnt++;
}
}
K r u s k a l Kruskal Kruskal 的理论依据: 图上唯一最短边一定出现在最小生成树中
如果改最短边不出现在这颗最小生成树上,那么将这条边加入最小生成树,会构成一个环,任意去除环上的一条非最短边,肯定会构成一个更小的生成树。
[USACO08OCT]Watering Hole G
这题需要引入一个 0 0 0 点
将建造水井所需的 w i w_i wi 视为从 0 0 0 点到该点长为 w i w_i wi 的边
代码
K r u s k a l Kruskal Kruskal 同样可以求最大生成树,理论依据也和最小生成树一样,只需改变排序顺序即可
[NOIP2013 提高组] 货车运输 (最大生成树)
这题先求出最大生成树(不止一颗),再在最大生成树上跑 倍增 lca 求出倍增经过所有边的最小权值
那么在 dfs 的过程中就需要维护 k 级祖先内分最小边权
//最大生成树
struct Edge{
int u,v,w;
bool operator < (const Edge &x)const{
return w>x.w;
}
}e[MAXM];
void Kruskal()
{
for(int i=1;i<=n;i++) f[i]=i;
sort(e+1,e+m+1);
for(int i=1;i<=m;i++)
{
int u=e[i].u,v=e[i].v;
int rt_u=find(u),rt_v=find(v);
if(rt_u==rt_v) continue;
f[rt_u]=rt_v;
G[u].push_back(Node{v,e[i].w});
G[v].push_back(Node{u,e[i].w});
}
}
//lca
void dfs(int u,int fa)
{
dep[u]=dep[fa]+1; p[u][0]=fa; vis[u]=1;
for(int i=1;i<=lg[dep[u]];i++)
{
p[u][i]=p[p[u][i-1]][i-1];
w[u][i]=min(w[u][i-1],w[p[u][i-1]][i-1]);
}
for(int i=0;i<G[u].size();i++)
{
int v=G[u][i].to;
if(v==fa || vis[v]) continue;
w[v][0]=G[u][i].w;
dfs(v,u);
}
}
void lca(int x,int y)
{
if(find(x) != find(y)) {
puts("-1");return ;
}
int res=INF;
if(dep[x]<dep[y]) swap(x,y);
while(dep[x]>dep[y])
{
res=min(res , w[x][lg[dep[x]-dep[y]]] );
x=p[x][lg[dep[x]-dep[y]]];
}
if(x==y) { cout<<res<<endl; return; }
for(int k=lg[dep[x]]; k>=0 ; k--)
{
if(p[x][k] != p[y][k])
{
res=min3(res,w[x][k],w[y][k]);
x=p[x][k], y=p[y][k];
}
}
res=min3(res,w[x][0],w[y][0]);
cout<<res<<endl;
}
for(int i=1;i<=n;i++) {
if(!vis[i]) {
dfs(i,0); w[i][0]=INF;
}
}
这里不止一颗树,不止一个根节点,开个 vis 记录一下
[BJWC2010] 严格次小生成树
严格次小生成树即严格小于当前最小生成树的权值和
(如果有两条相同权值的边,那么 非严格次小生成树权值和最小生成树一样)
1. 1. 1. 先求出 MST
2. 2. 2. 选取一条未选边 (白边)
该白边一定大于等于任意一条红边,因为如果白色边小于某条红色边,那么 MST 一定不为当前 (黄色) 形态
选取该边会构成一个环,那么需要删去一条红色边
现在加进来一条比红色边都大的边而又要使新的生成树最小,那么显然要删去最长的边
但是删去最长的边的权值若和该红色边权值相同,那么就不是严格次小生成树了,所以还需要维护次短边
也就是维护 倍增 lca 维护 k 级祖先的最大值和次大值
代码
(注意开long long 以及自环特判)
01分数规划入门题目 [USACO01OPEN]Earthquake
注意,这里等号要变成大于等于,因为 ans 是最终的答案(最大的方案),肯定要大于等于任意一种方案
二分这个 ans
(可以二分的性质:答案某一侧全为合法,另一侧全都是不合法)
将 a n s ∗ t i + C i ans*t_i + C_i ans∗ti+Ci 作为一条边的权值,跑最小生成树,看最小生成树的权值和是否小于等于 F F F
如果 最小生成树的权值和大于F, 那么应该缩小 ans 继续二分 ,反之放大
#include
using namespace std;
const double delta=1e-9;
const int MAXN=405;
const int MAXM=1e4+5;
struct Edge{
int u,v;
double c,t,w;
bool operator < (const Edge &x) const{
return w<x.w;
}
}e[MAXM];
int n,m,F,f[MAXN];
int find(int x)
{
if(f[x]==x) return x;
return f[x]=find(f[x]);
}
double Kruskal(double ans)
{
double ret=0; int cnt=0;
for(int i=1;i<=n;i++) f[i]=i;
for(int i=1;i<=m;i++) e[i].w=1.0*e[i].t*ans+1.0*e[i].c;
sort(e+1,e+m+1);
for(int i=1;i<=m;i++)
{
int rt_u=find(e[i].u),rt_v=find(e[i].v);
if(cnt==n-1) break;
if(rt_u==rt_v) continue;
ret+=e[i].w; cnt++;
f[rt_u]=rt_v;
}
return ret;
}
int main()
{
cin>>n>>m>>F;
for(int i=1;i<=m;i++)
{
cin>>e[i].u>>e[i].v>>e[i].c>>e[i].t;
}
double l=0,r=2e9;
while(l+delta<r)
{
double mid=(l+r)/2;
if(Kruskal(mid)>F) r=mid-delta;
else l=mid+delta;
}
printf("%.4lf",l);
return 0;
}
这里学到了二分 double 型变量的办法,就是搞一个 d e l t a = 1 0 − 9 delta=10^{-9} delta=10−9 很小的数
树的颜色
问在某某子树下,指定颜色的点有几个?
例如,现在问在 a a a 的子树中,红色的结点有多少个
如图,按 dfs 序遍历,进入 a a a 之前红色结点个数为 2 个,离开 a a a 点红色结点为 6 个,那么在 a a a 的子树中,红色的结点有 6 − 2 = 4 6-2=4 6−2=4 个
#include
using namespace std;
const int MAXN=2e5+5;
int n,col[MAXN],cnt[MAXN],ans[MAXN];
vector <int> son[MAXN];
void dfs(int u)
{
cnt[col[u]]++;
int before=cnt[col[u]];
for(auto v:son[u])
{
dfs(v);
}
ans[u]=cnt[col[u]]-before;
}
int main()
{
cin>>n;
for(int i=2,f;i<=n;i++)
{
cin>>f;
son[f].push_back(i);
}
for(int i=1;i<=n;i++)
cin>>col[i];
dfs(1);
for(int i=1;i<=n;i++)
cout<<ans[i]<<" ";
return 0;
}
图中 ,
D i D_i Di 示结点 i i i 的深度 (depth)
W a W_a Wa 表示观测点 a a a 的观测时间
B i B_i Bi 表示到达 i i i 的出发时间 (Begining)
不难得到,如果结点 i i i 能被观测点 a a a 观测到,应满足 D i + B i = D a + W a D_i+B_i=D_a+W_a Di+Bi=Da+Wa
令点 i i i 的 D i + B i D_i+B_i Di+Bi 为 k e y i key_i keyi
那么,对于这张图,需统计 a a a 的子树上有多少个结点的 k e y i key_i keyi 恰好等于 3 3 3
先模拟一下样例,左边表示各观察点,右图表示各玩家
对于 1 1 1 号点来说 有两个 B i + D i ( 即 k e y i ) = W i + D i B_i +D_i ({即 key_i})=W_i+D_i Bi+Di(即keyi)=Wi+Di 因此一号点观察到两名玩家
图中不同的 k e y key key 对应不同的颜色
若一个玩家从 S i S_i Si 到 T i T_i Ti ,令他们的 lca 为 P i P_i Pi
如果 S i S_i Si 到 P i P_i Pi 是向上走,那么 B i B_i Bi 就等于 0,因此 k e y i = D i + B i = D i + 0 key_i=D_i+B_i=D_i+0 keyi=Di+Bi=Di+0
我们将 < S i , P i >
表示为 < S i , D i + 0 , d e l t a = + 1 >
以及 < P i , D i + 0 , d e l t a = − 1 >
若 P i P_i Pi 到 T i T_i Ti 是向上走,注意 B i B_i Bi 就不等于 0, k e y i = D i + B i key_i=D_i+B_i keyi=Di+Bi
< P i , T i >
表示为 < P i , D i + B i , d e l t a = + 1 >
以及 < T i , D i + B i , d e l t a = − 1 >
因此问 a a a 子树中可观测的点就相当于问 a a a 子树中 d e l t a delta delta 的之和
就转换成了树上差分
在 DAG (有向无环图) 上
拓扑序:
A B F D C E ABFDCE ABFDCE
D A C B F E DACBFE DACBFE
处理方式:将入度为 0 的点进入队列,将出度为0的点踢出队列
每次去除队首,减少队首出边的入度
for(int i=1;i<=n;i++)
{
if(rd[i]==0) q.push(i);
}
while(!q.empty())
{
int hd=q.front();
q.pop();
for(auto v:G[hd])
{
rd[v]--;
if(rd[v]==0) q.push(v);
}
}
如果要求按某关键字从小到大,则使用 priority_queue
Roads and Planes G
举个例子,黄色的边表示公路,蓝色的边表示航线
若需要求出 1 号点到其他结点的最短距离,我们先用 d i j i s k t r a dijisktra dijisktra 求出 1、2、3、4 这一连通块中 1 到 2、3、4 的最短距离
然后就可以用 [ 1到 3 的最短路 + 航线的费用 ] 去更新 1 到 9 的距离,同样地,更新 6 的距离
因为题目规定:
如果有一条航线可以从 A i A_i Ai 到 B i B_i Bi ,那么保证不可能通过一些道路和航线从 B i B_i Bi 回到 A i A_i Ai
所以,通过各航线形成的图一定是一张 D A G DAG DAG ( 有 向 无 环 图 ) ({有向无环图}) (有向无环图)
将图分成各联通块,在每个连通块内跑 d i j i s k t r a dijisktra dijisktra 求出各个块内的最短路,并按照拓扑排序按遍历各个联通块
最后 在 D A G DAG DAG 上更新答案即可
#include
using namespace std;
const int MAXN=3e4+5;
const int INF=0x3f3f3f3f;
int n,r,p,s,col[MAXN],cnt;
struct Node{
int to,w;
bool operator < (const Node &x) const{
return x.w<w;
}
};
vector <Node> R[MAXN];
vector <Node> P[MAXN];
vector <int> G[MAXN]; // 存 某一连通块 的各结点
void dfs(int u,int num)
{
col[u]=num;
G[num].push_back(u);
for(auto v:R[u])
{
if(!col[v.to])
dfs(v.to,num);
}
}
int dis[MAXN];
bool vis[MAXN];
priority_queue <Node> q1;
void Dijisktra(int x)
{
// 表示 x 号 连通块
for(auto v:G[x])
if(dis[v]<INF) // 拿已经更新过的点去作为起点
q1.push(Node{v,dis[v]});
while(!q1.empty())
{
Node hd=q1.top(); q1.pop();
int u=hd.to;
if(vis[u]) continue; vis[u]=true;
for(auto e:R[u])
{
int v=e.to;
if(dis[u]+e.w<dis[v])
{
dis[v]=hd.w+e.w;
if(!vis[v])
q1.push(Node{v,dis[v]});
}
}
for(auto e:P[u]) //更新航线
{
int v=e.to;
dis[v]=min(dis[v],dis[u]+e.w);
}
}
}
int rd[MAXN];
void Topo()
{
for(int i=1;i<=n;i++)
{
for(auto v:P[i])
rd[col[v.to]]++;
}
queue <int> q2;
for(int i=1;i<=cnt;i++)
{
if(!rd[i])
{
q2.push(i);
}
}
while(!q2.empty())
{
int hd=q2.front(); q2.pop();
Dijisktra(hd); //每次在队首的联通块中进行dijisktra 更新最短路
for(auto i:G[hd]) //将当前连通块能通过航线到达的所有城市入度-1
{
for(auto e:P[i])
{
int v=e.to;
rd[col[v]]-=1;
if(rd[col[v]]==0) q2.push(col[v]);
}
}
}
}
int main()
{
cin>>n>>r>>p>>s;
for(int i=1;i<=n;i++)
dis[i]=INF;
dis[s]=0;
for(int i=1;i<=r;i++)
{
int u,v,w;
cin>>u>>v>>w;
R[u].push_back((Node){v,w});
R[v].push_back((Node){u,w});
}
for(int i=1;i<=p;i++)
{
int u,v,w;
cin>>u>>v>>w;
P[u].push_back((Node){v,w});
}
for(int i=1;i<=n;i++)
{
if(!col[i])
dfs(i,++cnt);
}
Topo();
for(int i=1;i<=n;i++)
{
if(dis[i]==INF) puts("NO PATH");
else printf("%d\n",dis[i]);
}
return 0;
}
桥:去掉该边图不连通的边叫做桥
割点: 去掉该点图不连通的点叫做割点
枚举每一条 边/点 判断图是否连通
时间复杂度为 O ( E ∗ V 2 ) O(E*V^2) O(E∗V2) 或 O ( V ∗ V 2 ) O(V*V^2) O(V∗V2)
l o w u low_u lowu : u u u 及其子树内所有节点能回到最早祖先的时间戳
d f n u dfn_u dfnu : u u u 点的 d f s dfs dfs 序的时间戳
对于一个节点 u u u ,他的儿子为 v v v
c a s e 1 : case1: case1:
如果 l o w v ≥ d f n u low_v \geq dfn_u lowv≥dfnu , 那么 v v v 无法不经过 u u u 到达 u u u 的祖先 , 若将 u u u 删去,那么图显然无法联通, u u u 是割点
c a s e 2 case2 case2
u u u 是 r o o t root root , 如果 u u u 的儿子 ≥ 2 \geq 2 ≥2 ,那么两个儿子无法联通, u u u 是割点
对于一条边 ( u , v ) (u,v) (u,v)
如果 l o w v > d f n u low_v>dfn_u lowv>dfnu 那么 v v v 无法不经过该边而到达 u u u 及 u u u 的祖先,将该边去掉,图不连通, ( u , v ) (u,v) (u,v) 为一条桥
桥
【模板】桥
T a r j a n Tarjan Tarjan 算法
void dfs(int u,int fa)
{
dfn[u]=++idx;
low[u]=dfn[u];
for(int i=0;i>n>>m;
for(int i=1;i<=m;i++) {
int u,v; cin>>u>>v;
G[u].push_back(v);
G[v].push_back(u);
}
dfs(1,0);
割点
【模板】割点
void dfs(int u,int fa)
{
low[u]=dfn[u]=++idx;
int child=0;
for(int i=0;i=dfn[u] && fa!=0 && !flag[u]) flag[u]=1,cnt++; // case1: 非 root && low[v]>=dfn[u]
}
if(v!=fa && dfn[v]) low[u]=min(low[u],dfn[v]);
}
if(child>=2 && fa==0 && !flag[u]) flag[u]=1,cnt++; // case2: root && 有两个儿子及以上
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v;
cin>>u>>v;
G[u].push_back(v);
G[v].push_back(u);
}
for(int i=1;i<=n;i++)
if(!dfn[i])
{idx=0; dfs(i,0); } //root
cout<
强联通分量 ( scc ): 对于有向图的一个块,若块中的点两两之间能互相到达,那么这个块是一个强连通分量( scc )
开一个栈用于储存当前的强连通分量
对于一个节点 u u u ,
如果 l o w u low_u lowu == d f n u dfn_u dfnu 表示该点及其子树内所有节点能回到最早的祖先是 u u u 自己,那么 u u u 就是一个强连通分量的开端,将当前 scc 的所有结点弹出栈
如果儿子 v v v 未被访问,访问 v v v 并更新 l o w low low
如果儿子 v v v 已经被访问,并且在栈中(说明 v v v 属于当前 scc),更新 l o w low low
如果儿子 v v v 已经被访问,并且不在栈中(说明 v v v 已经被搜索过并且是以前某个scc中的结点),不需要操作
//Tarjan 模板
int s[MAXN],top,dfn[MAXN],low[MAXN],idx;
bool in[MAXN];
int scc[MAXN],num,siz[MAXN];
//scc[u]表示 u 号 scc 编号
//siz[num] 表示第 num 个 scc 的结点个数
void dfs(int u)
{
low[u]=dfn[u]=++idx;
s[++top]=u, in[u]=1;
for(int i=0;i<G[u].size();i++)
{
int v=G[u][i];
if(!dfn[v])
{
dfs(v);
low[u]=min(low[u],low[v]);
}
if(dfn[v] && in[v])
low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u])
{
++num;
while(s[top]!=u)
{
scc[s[top]]=num; siz[num]+=1;
in[s[top]]=0; top-=1;
}
scc[s[top]]=num; siz[num]+=1;
in[s[top]]=0; top-=1;
}
}
USACO 受欢迎的牛 G (scc 模板)
朴素算法,枚举每一个点,判断是否被所有人喜欢,复杂度为 O ( n 3 ) O(n^3) O(n3)
例:
图中 1、 2、 3、 4号奶牛是最受欢迎的
这张图有两个强连通分量,蓝色的一块以及黄色的一块
对于一个强连通分量 :
scc 内的所有点都互相喜欢
如果该 scc1 有一条出边指向其他 scc2( 即出度不为 0 ),那么被指向的 scc2 不可能有边指向该 scc1( 因为如果互相有边就会被包括在当前该 scc1 中 ),该 scc1 内的所有点就不满足对所有点喜欢
如果出度为 0 ,那么只要该 scc 与其他点均联通, 那么该 scc 内的所有点都是最受欢迎的奶牛
如图,此时虽然两个 scc 均满足出度为 0 ,但是没有最受欢迎的奶牛
因此,我们只需判断:满足出度为 0 的强联通分量的个数,如果只有一个则输出该 scc 内的结点个数
对于一条单向边 E ( u , v ) E(u,v) E(u,v) ,如果 u u u 和 v v v 不在同一 scc 中,那么起点所在的 scc 出度就加一
枚举所有边就可以判断每个 scc 的出度
#include
using namespace std;
const int MAXN=1e5+5;
vector <int> G[MAXN];
int s[MAXN],top,dfn[MAXN],low[MAXN],idx,de[MAXN];
//de: 出度
bool in[MAXN];
int scc[MAXN],num,siz[MAXN];
int n,m;
void dfs(int u)
{
dfn[u]=low[u]=++idx;
s[++top]=u; in[u]=1;
for(int i=0;i<G[u].size();i++)
{
int v=G[u][i];
if(dfn[v] && in[v])
low[u]=min(low[u],dfn[v]);
if(!dfn[v])
{
dfs(v);
low[u]=min(low[u],low[v]);
}
}
if(dfn[u]==low[u])
{
++num;
while(s[top]!=u)
{
scc[s[top]]=num; siz[num]+=1;
in[s[top]]=0; top-=1;
}
scc[s[top]]=num; siz[num]+=1;
in[s[top]]=0; top-=1;
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v;
cin>>u>>v;
G[v].push_back(u);
}
for(int i=1;i<=n;i++)
if(!dfn[i])
dfs(i);
for(int u=1;u<=n;u++)
for(auto v : G[u])
if(scc[u]!=scc[v])
de[scc[v]]++;
int ans,cnt=0;
for(int i=1;i<=num;i++)
if(de[i]==0)
ans=siz[i],cnt++;
printf("%d",cnt==1 ? ans : 0);
return 0;
}
2 − S A T 2-SAT 2−SAT: 对于每个要求,有两个条件,每个要求需满足其中一个,求解。该问题叫做 2 − S A T 2-SAT 2−SAT 问题
如 :
x = 1 x=1 x=1 就是这组 2 − S A T 2-SAT 2−SAT 问题的解
对于要求 1,如果 x ≠ 1 x\not =1 x=1 那么 x = 0 x=0 x=0 就一定成立
对于一个条件 a a a,我们记 a a a 表示满足条件, − a -a −a 表示表示不满足条件
对于一个要求,要求 a a a 与 b b b 成立
那么,
如果 a a a 不成立,那么 b b b 一定成立
相当于 如果 − a -a −a 成立,那么 b b b 一定成立
现有要求
等价于
等价于
如图, a a a 与 − a -a −a 在同一强联通分量上,说明
如果 a a a 满足,则 a a a 不满足,矛盾
因此 该 2 − S A T 2-SAT 2−SAT 问题无解
2-SAT 入门题 [JSOI2010] 满汉全席 (判断有无解)
#include
using namespace std;
const int MAXN=1e3+5;
int n,m;
vector <int> G[MAXN];
int st[MAXN],top;
bool in[MAXN];
int dfn[MAXN],low[MAXN],idx;
int scc[MAXN],cnt;
void Tarjan(int u)
{
dfn[u]=low[u]=++idx;
st[++top]=u; in[u]=1;
for(auto v:G[u])
{
if(!dfn[v])
{
Tarjan(v);
low[u]=min(low[u],low[v]);
}
if(dfn[v] && in[v]) low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u])
{
cnt++;
while(st[top]!=u)
{
scc[st[top]]=cnt;
in[st[top]]=0; top-=1;
}
scc[st[top]]=cnt;
in[st[top]]=0; top-=1;
}
}
int main()
{
int K;
cin>>K;
while(K--)
{
cin>>n>>m;
for(int i=1;i<=2*n;i++)
{
G[i].clear(),scc[i]=dfn[i]=low[i]=in[i]=st[i]=0;
}
idx=top=cnt=0;
while(m--)
{
string sa,sb;
cin>>sa>>sb;
int a=0,b=0;
for(int i=1;i<sa.length();i++) a=a*10+sa[i]-'0';
for(int i=1;i<sb.length();i++) b=b*10+sb[i]-'0';
// m: x h: x+n
if(sa[0]=='m' && sb[0]=='m')
{
G[a+n].push_back(b);
G[b+n].push_back(a);
}
if(sa[0]=='h' && sb[0]=='h')
{
G[a].push_back(b+n);
G[b].push_back(a+n);
}
if(sa[0]=='m' && sb[0]=='h')
{
G[a+n].push_back(b+n);
G[b].push_back(a);
}
if(sa[0]=='h' && sb[0]=='m')
{
G[a].push_back(b);
G[b+n].push_back(a+n);
}
}
for(int i=1;i<=2*n;i++)
{
if(!dfn[i]) Tarjan(i);
}
bool flag=true;
for(int i=1;i<=n;i++)
{
if(scc[i]==scc[i+n])
{
flag=false;
puts("BAD");
break;
}
}
if(flag) puts("GOOD");
}
}
【模板】2-SAT 问题
此题还需求出满足 2 − S A T 2-SAT 2−SAT 问题的解
此题 令 x i = 1 x_i=1 xi=1 为 a a a , x i = 1 x_i=1 xi=1 相当于 a a a, x i = 0 x_i =0 xi=0 相当于 − a -a −a
在得出问题有解并求出 scc 后,我们按 T a r j a n Tarjan Tarjan 得到的 scc 的顺序将图排序( T a r j a n Tarjan Tarjan 求得的顺序其实就是 逆拓扑序)
对于 a a a 和 − a -a −a ,只有一个成立,我们将先访问到的状态设为成立
如果 a a a 比 − a -a −a 先访问到,使 x i = 1 x_i=1 xi=1
如果 − a -a −a 比 a a a 先访问到,使 x i = 0 x_i=0 xi=0
#include
using namespace std;
const int MAXN=1e7+5;
int n,m;
vector <int> G[MAXN];
int st[MAXN],top;
bool in[MAXN];
int dfn[MAXN],low[MAXN],idx;
int scc[MAXN],cnt;
void Tarjan(int u)
{
dfn[u]=low[u]=++idx;
st[++top]=u; in[u]=1;
for(auto v:G[u])
{
if(!dfn[v])
{
Tarjan(v);
low[u]=min(low[u],low[v]);
}
if(dfn[v] && in[v]) low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u])
{
cnt++;
while(st[top]!=u)
{
scc[st[top]]=cnt;
in[st[top]]=0; top-=1;
}
scc[st[top]]=cnt;
in[st[top]]=0; top-=1;
}
}
int main()
{
cin>>n>>m;
while(m--)
{
int i,j,xi,xj;
cin>>i>>xi>>j>>xj;
if(xi && xj)
{
G[i].push_back(j+n);
G[j].push_back(i+n);
}
if(!xi && xj)
{
G[i+n].push_back(j+n);
G[j].push_back(i);
}
if(!xi && !xj)
{
G[i+n].push_back(j);
G[j+n].push_back(i);
}
if(xi && !xj)
{
G[i].push_back(j);
G[j+n].push_back(i+n);
}
}
for(int i=1;i<=2*n;i++)
{
if(!dfn[i]) Tarjan(i);
}
for(int i=1;i<=n;i++)
{
if(scc[i]==scc[i+n])
{
puts("IMPOSSIBLE");
return 0;
}
}
puts("POSSIBLE");
for(int i=1;i<=n;i++)
{
if(scc[i]>scc[i+n]) printf("1 ");
else printf("0 ");
}
return 0;
}
【模板】缩点
该题问点权最大的一条路径,如果路径经过了一个环,那么环上所有点权显然都要被加到答案中,环内所有点都要访问
那么该图就等价于
大大减少了访问的节点数,这便是 缩点
//缩点
void dfs(int u)
{
dfn[u]=low[u]=++idx;
s[++top]=u, in[u]=1;
for(auto v:G[u])
{
if(dfn[v] && in[v]) low[u]=min(low[u],dfn[v]);
if(!dfn[v])
{
dfs(v);
low[u]=min(low[u],low[v]);
}
}
if(low[u]==dfn[u])
{
cnt++;
while(s[top]!=u)
{
scc[s[top]]=cnt; sum[cnt]+=a[s[top]];
in[s[top]]=0; top-=1;
}
scc[s[top]]=cnt; sum[cnt]+=a[s[top]];
in[s[top]]=0; top-=1;
}
}
在缩点之后,问题就转化为了求最大点权和,在此提供两种遍历求解方式( 类似搜索的dfs 与 bfs )
void dfs2(int u)
{
if(mem[u]) return ;
mem[u]=1;
int tmp=0; f[u]=sum[u];
for(auto v:e[u])
{
dfs2(v);
tmp=max(tmp,f[v]);
}
f[u]+=tmp;
}
int ans=0;
for(int i=1;i<=cnt;i++)
if(!mem[i])
{
dfs2(i);
ans=max(ans,f[i]);
}
for(int i=1;i<=n;i++)
{
if(rd[i]==0) q.push(i);
}
while(!q.empty())
{
int hd=q.front();
q.pop();
for(auto v:G[hd])
{
rd[v]--;
if(rd[v]==0) q.push(v);
}
}
- P2002 消息扩散
在一个 scc 中的点只要其中一个收到消息,则 scc 中的所有点都可以收到消息,因此考虑缩点
memset(check,1,sizeof(check)); //先全部设置
for(int i=1;i<=n;i++)
for(auto v:G[i])
if(scc[i]!=scc[v]) //若两个不同的scc间有边,则被指向的 scc 就不需要设置
check[scc[v]]=0;
int ans=0;
for(int i=1;i<=cnt;i++)
if(check[i])
ans++;
cout<
- P1262 间谍网络
根据题意,如果收买了某一个间谍,那么便可以控制该间谍情报所指向的间谍
如图,若收买其中任意一个间谍,就可以控制图上的所有间谍
因此,如果有一个环,我们就需要选取一个环上收买价格最低的点
所以,我们考虑缩点
对于一个 s c c scc scc ,若缩点后其他的 s c c scc scc 有边指向该 s c c scc scc ,那么就不需要该 s c c scc scc 上的间谍
那么,统计每个点的入度,若入度为 0 0 0 表示该 s c c scc scc 中至少一个点需要被收买,则将该 s c c scc scc 中的最小值统计入答案
而判断是否能全部控制,只需要从每个可以被收买的点开始 T a r j a n Tarjan Tarjan ,若有点未被访问,则无法全部控制
代码