本周主要讲了三个图的具体应用
- 差分约束
- 拓扑排序
- 强连通分量
并选取例题进行讲解巩固
差分约束——最短/长路问题
概述
简要概括就是一组不等式作为约束条件,求出一组最小解使得所有约束条件都得到满足。
不等式需要满足的条件:
- 每个约束条件是由其中两个变量做差构成的,形如xi-xj<=ck
求解形如 <= 的差分约束系统,可以转化为图论中的单源最短路问题。
对于差分约束中的每一个不等式约束xi−xj≤ck都可以移项变形为xi<=xj+ck如果令𝑐k=𝑤(𝑖,𝑗),𝑑𝑖𝑠𝑖=𝑥i,𝑑𝑖𝑠𝑗=𝑥j,那么原式变为𝑑𝑖𝑠𝑖≤ 𝑑𝑖𝑠𝑗+𝑤𝑖,𝑗,与最短路问题中的松弛操作相似
所以可以将变量xi视为图中的一个节点,对于每个不等式约束xi-xj<=ck则是从节点j到节点i连一条长度为ck的有向边,令x1=0,那么xi=dis[i]即为差分约束的一组解。
问题
由于在求解最短路的过程中出现存在负环或者终点不可达的情况,那么 在求解差分约束的过程中也会存在这样的问题
- 存在负环
如果路径中存在负环则表现为最短路的无限小,即最短路不存在,在不等式约束上表现为𝑥i−𝑥j≤𝑇中的T为无限小,得出的结论就是𝑥i的结果不存在
可以通过Bellman算法或者队列优化的Bellman算法(SPFA)来判断负环是否存在
- 终点不可达
这种情况表明变量𝑥i与变量1之间没有约束关系,xi的结果可以是无限大,对应代码中为dis[i]=inf
实际结果
- ≤——最短路——最大解
- ≥——最长路——最小解
例题 T1区间选点
题目描述
给定一个数轴上的n个区间,要求在数轴上选取最少的点使得第i个区间里至少有ci个点。
思路
利用差分约束系统进行求解。首先构造不等式组:
记sum[i]为数轴上[0,i]之间选点的个数,所以对任意区间[ai,bi]需要满足:
- sum[bi]-sum[ai-1]>=ci
同时还需要保证sum是有意义的,即区间右端点向右+1,sum至多加1
- 0<=sum[i]-sum[i-1]<=1
由于要求最少的点即求该差分约束系统的最小解,转化为≥不等式组跑最长路,答案为 sum[max{𝑏i}]
#include <stdio.h>
#include <algorithm>
#include <string.h>
#include <queue>
#include <vector>
using namespace std;
const int inf = 1e8;
const int N = 500005;
int n, dis[N], vis[N],t,r;//r最右端点
int head[N], tot;
struct Edge{
int u,v,next,w;
}e[5000005];
void add(int u,int v,int w)
{
tot++;
e[tot].u=u;
e[tot].v=v;
e[tot].w=w;
e[tot].next=head[u];
head[u]=tot;
}
void spfa(int s) {
queue<int> q;
q.push(s);
dis[s] = 0;
vis[s] = 1;
while(!q.empty()) {
int u = q.front();
q.pop();
vis[u] = 0;
for(int i = head[u];i; i = e[i].next) {
int v = e[i].v;
if(dis[v] < dis[u] + e[i].w) {
dis[v] = dis[u] + e[i].w;
if(!vis[v]) {
q.push(v);
vis[v] = 1;
}
}
}
}
}
void initial()
{
memset(dis,-0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
memset(head,0,sizeof(head));
tot=0;
}
int main()
{
initial();
scanf("%d",&n);
int a=0,b=0,c=0;
for(int i=0;i<n;i++)
{
scanf("%d %d %d",&a,&b,&c);
add(a,b+1,c);
r=max(r,b+1);
}
for(int i=1;i<=r;i++)
{
//0<=si-s(i-1)<=1
add(i-1,i,0);
add(i,i-1,-1);
}
spfa(0);
printf("%d",dis[r]);
return 0;
}
拓扑排序
描述
拓扑排序的目标是将所有节点排序,使得排在前面的节点不能依赖于排在后面的节点。
Kahn算法
- 将入度为0的点组成一个集合S
- 每次从S里面取出一个顶点u(可以随便取)放入L, 然后遍历顶点u的所有边(u, v), 并删除之,并判断如果该边的另一个顶点v,如果在移除这一条边后入度为0, 那么就将这个顶点放入集合S中。不断地重复取 出顶点然后重复这个过程……
- 最后当集合为空后,就检查图中是否存在任何边。如果有,那么这个图一定有环路,否则返回L,L中顺序就是拓扑排序的结果
T2 猫猫向前冲
题目描述
N只猫猫,每场比赛可以得到两只猫猫的比赛结果,最后按照排名发放奖品。一直每场比赛的结果,求字典序最小的名次序列。
思路
猫猫之间的胜负关系可以构成一张有向无环图。a赢了b即边a->b
字典序最小则使用优先队列
#include <stdio.h>
#include <algorithm>
#include <string.h>
#include <queue>
#include <vector>
using namespace std;
int n,m,tot;
int in_deg[505],head[505];
struct Edge
{
int u,v,next;
}e[500*500+10];
void add(int u,int v)
{
tot++;
e[tot].u=u;
e[tot].v=v;
e[tot].next=head[u];
head[u]=tot;
in_deg[v]++;
}
void initial()
{
memset(e,0,sizeof(e));
memset(in_deg,0,sizeof(in_deg));
memset(head,0,sizeof(head));
tot=0;
}
void toposort()
{
priority_queue<int,vector<int>,greater<int> > q;
for(int i=1;i<=n;i++)
{
if(in_deg[i]==0)
{
q.push(i);
}
}
vector<int> ans;
while(!q.empty())
{
int u=q.top();
q.pop();
ans.push_back(u);
for(int i=head[u];i;i=e[i].next)
{
int y=e[i].v;
if(--in_deg[y]==0) q.push(y);
}
}
if(ans.size()==n)
{
for(int i=0;i<n;i++)
{
if(i!=n-1)
printf("%d ",ans[i]);
else printf("%d\n",ans[i]);
}
}
}
int main()
{
while(~scanf("%d %d",&n,&m))
{
initial();
for(int i=0;i<m;i++)
{
int p1=0,p2=0;
scanf("%d %d",&p1,&p2);
add(p1,p2);
}
toposort();
}
return 0;
}
强连通分量SCC
SCC定义
极大的强连通子图。
强连通:有向图G中任意两个节点连通
DFS序列
- 前序序列:第一次到达点x的次序,用d[x]表示
- 后序序列:x点遍历完成的次序,即回溯时间,用f[x]表示
- 逆后序序列:后序序列的逆序
Kosaraju算法——求SCC
第一遍DFS确定原图的逆后序序列
第二遍DFS在反图中按照逆后序序列进行遍历,每次由起点遍历到的点即构成一个SCC
tips:反图与原图具有一样的SCC
板子:
int n,c[N],dfs[N],vis[N],dcnt,scnt;
//dcnt-dfs序计数,scnt-scc计数
//dfs[i]-dfs后序列中第i个点
//c[i]-i号点所在scc编号
vector<int> G1[N],G2[N]; //G1-原图,G2-反图
void dfs1(int x)
{//求DFS后序
vis[x] = 1;
for (auto y : G1[x])
if (!vis[y])dfs1(y);
dfs[++dcnt] = x;
}
void dfs2(int x)
{//求SCC
c[x] = scnt;
for (auto y : G2[x])
if (!c[y])dfs2(y);
}
void kosaraju(){
//初始化
dcnt=scnt=0;
memset(c,0,sizeof(c));
memset(vis,0,sizeof(vis));
//第一遍dfs
for(int i=1;i<=n;i++)
if(!vis[i]) dfs1(i);
//第二遍dfs
for(int i=n;i>=1;i--)
if(!c[dfn[i]]) ++scnt,dfs2(dfn[i]);
}
T3 班长竞选
题目描述
N位同学,意见AB表示A认为B合适,同时意见具有传递性,即A->B,B->C则A->C
共M条意见,求最高票数及所有得票最高的同学
思路
求出 SCC 并缩点,即将互相可达与单向可达分开考虑。缩点后,不难发现对于属于第 i 个 SCC 的点来说( 令 SCC[i] 表示第 i 个 SCC 中点的个数),答案分为两部分:
- 当前 SCC 中的点,ans += SCC[i] – 1(去除自己)
- 其它 SCC 中的点 ,SUM ( SCC[j] ),其中 j 可到达 i
然后可以发现最后答案一定出现在原图出度为 0 的 SCC 中,即用反图缩点图跑dfs,记录最大值所在的点即可。
具体操作
首先存好正图和反图,然后加载kosaraju算法计算SCC(强连通分量):从0号点开始,第一遍dfs求DFS后序;然后按照DFS逆后序顺序,第二遍dfs求SCC,此时SCC[i]中存储的是i号点所在的SCC编号。
然后进行缩点操作:用反图缩点图进行投票传递计算DFS。
之后遍历反图缩点图中每个入度为0的点进行bfs,同时记录最大值,记得答案要-1,除去自己给自己的投票。
最后循环输出答案即可。
坑(代码不规范流的泪)
在add(int u,int v,Edge* e,int* head,int& tot)函数中。。tot应该是一个传入的全局变量,需要加**&**,如果不加的话每一次都是0进入。。会导致死循环
#include <Stdio.h>
#include <algorithm>
#include <string.h>
using namespace std;
int t=0,n=0,m=0,tot1=0,tot2=0,tot3=0,dcnt=0,scnt=0;
int head1[5005],head2[5005],head3[5005];
int vis[5005],vis2[5005],dfn[5005],c[5005];
int in_deg[5005],sccnum[5005],ans[5005];
struct Edge
{
int u,v,next;
}e1[30005],e2[30005],e3[30005];
void add(int u,int v,Edge* e,int* head,int& tot)
{
tot++;
e[tot].u=u;
e[tot].v=v;
e[tot].next=head[u];
head[u]=tot;
}
void initial()
{
memset(head1,0,sizeof(head1));
memset(head2,0,sizeof(head2));
memset(head3,0,sizeof(head3));
memset(e1,0,sizeof(e1));
memset(e2,0,sizeof(e2));
memset(e3,0,sizeof(e3));
memset(sccnum,0,sizeof(sccnum));
memset(vis2,0,sizeof(vis2));
memset(in_deg,0,sizeof(in_deg));
memset(ans,0,sizeof(ans));
tot1=0;
tot2=0;
tot3=0;
}
void dfs1(int x)
{
vis[x]=1;
for(int p=head1[x];p;p=e1[p].next)
{
int y=e1[p].v;
if(!vis[y]) dfs1(y);
}
dfn[++dcnt]=x;
}
void dfs2(int x)
{
c[x]=scnt;
for(int i=head2[x];i;i=e2[i].next)
{
int y=e2[i].v;
if(!c[y]) dfs2(y);
}
}
void kosaraju()
{
dcnt=0,scnt=0;
memset(c,0,sizeof(c));
memset(dfn,0,sizeof(dfn));
memset(vis,0,sizeof(vis));
for(int i=0;i<n;i++)
{
if(!vis[i])
{
dfs1(i);
}
}
for(int i=n;i>=1;i--)
{
if(!c[dfn[i]])
{
++scnt;
dfs2(dfn[i]);
}
}
for(int i=0;i<n;i++)
sccnum[c[i]]++;
}
void suodian()
{
for(int i=0;i<n;i++)
for(int j=head1[i];j;j=e1[j].next)
{
int y=e1[j].v;
if(c[i]!=c[y])
{
add(c[y],c[i],e3,head3,tot3);
in_deg[c[i]]++;
}
}
}
int tmpsum=0;
void dfs3(int x)
{
vis2[x]=1;
tmpsum+=sccnum[x];
for(int i=head3[x];i;i=e3[i].next)
{
int y=e3[i].v;
if(!vis2[y]) dfs3(y);
}
}
int main()
{
scanf("%d",&t);
for(int k=1;k<=t;k++)
{
initial();
scanf("%d %d",&n,&m);
int a=0,b=0;
for(int i=0;i<m;i++)
{
scanf("%d %d",&a,&b);
add(a,b,e1,head1,tot1);
add(b,a,e2,head2,tot2);
}
kosaraju();
suodian();
int lastnum=0;
for(int i=1;i<=scnt;i++)
{
if(in_deg[i]==0)
{
memset(vis2,0,sizeof(vis2));
tmpsum=0;
dfs3(i);
tmpsum-=1;
ans[i]=tmpsum;
}
}
int maxans=0;
for(int i=1;i<=scnt;i++)
{
if(maxans<ans[i]) maxans=ans[i];
}
bool flag[5005];
memset(flag,0,sizeof(flag));
for(int i=1;i<=scnt;i++)
{
if(ans[i]==maxans) flag[i]=1;
}
printf("Case %d: ",k);
printf("%d\n",maxans);
int ppp=0;
for(int i=0;i<n;i++)
{
if(flag[c[i]])
{
if(ppp==0)
{
printf("%d",i);
ppp++;
}
else printf(" %d",i);
}
}
if(k!=t) printf("\n");
}
return 0;
}