WEEK8——图的应用

本周主要讲了三个图的具体应用

  • 差分约束
  • 拓扑排序
  • 强连通分量

并选取例题进行讲解巩固

差分约束——最短/长路问题

概述

简要概括就是一组不等式作为约束条件,求出一组最小解使得所有约束条件都得到满足。
不等式需要满足的条件:

  • 每个约束条件是由其中两个变量做差构成的,形如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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值