WEEK(8)作业——差分约束、拓扑排序(Kahn算法)、SCC(强连通分量)、DFS序、Kosaraju算法

A-区间选点Ⅱ

题目描述

给定一个数轴上的 n 个区间,要求在数轴上选取最少的点使得第 i 个区间 [ai, bi] 里至少有 ci 个点
使用差分约束系统的解法解决这道题

Input

输入第一行一个整数 n 表示区间的个数,接下来的 n 行,每一行两个用空格隔开的整数 a,b 表示区间的左右端点。1 <= n <= 50000, 0 <= ai <= bi <= 50000 并且 1 <= ci <= bi - ai+1。

Output

输出一个整数表示最少选取的点的个数

Sample Input

5
3 7 3
8 10 3
6 8 1
1 3 1
10 11 1

Sample Output

6

解题思路

这道题要求用差分约束系统写。
先构造不等式组

  • 记sum[i]表示数轴上[0,i]之间选点的个数
  • 对于第i个区间[ai,bi]需要满足sum[bi]-sum[ai-1]>=ci,即sum[bi]>=ci+sum[ai-1],ai-1指向bi的边权重为ci,每次存储边,同时更新右端点

同时还需要保证sum是有意义的

  • 0<=sum[i]-sum[i-1]<=1,i-1指向i的边权重为0,i指向i-1的边权重为-1,从0至右端点,对每个单位区间添加这两条边

求解该差分约束系统的最小解,转化为 >= 不等式组用spfa跑最长路,答案为sum[max{bi}],b_max为右端点。

注意

因为存在不等式0<=sum[0]-sum[-1]<=1,所以将区间整体右移一个单位.

代码

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>

using namespace std;
 
const int inf=1e8;
const int N=50110;
const int M=50010*4;
int n,dis[N],head[N],vis[N],tot,b_max=0;
queue<int> q;

struct Edge {
	int to,nxt,w;
}e[M];

void add(int x,int y,int w) {
	e[++tot].to=y;
	e[tot].nxt=head[x];
	e[tot].w=w;
	head[x]=tot;
}

void init() {
	for(int i=0;i<=50010;++i) {
		dis[i]=-inf;
		vis[i]=0;
	}
	tot=0;
}

void spfa(int s) {
	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].nxt) {
			int v=e[i].to;
			if(dis[v]<dis[u]+e[i].w) {
				dis[v]=dis[u]+e[i].w;
				if(!vis[v]) {
					q.push(v);
					vis[v]=1;
				}
			}
		}
	}
}

int main() {
	scanf("%d",&n);
	int a=0,b=0,c=0;
	init();
	for(int i=1;i<=n;++i) {
		scanf("%d%d%d",&a,&b,&c);
		a += 1,b += 1;
		add(a-1,b,c);
		if(b_max<b) b_max=b;
	}
	for(int i=1;i<=b_max;++i) {
		add(i,i-1,-1);
		add(i-1,i,0);
	}
	spfa(0);
	printf("%d\n",dis[b_max]);
	return 0;
}

B-猫猫向前冲

题目描述

众所周知, TT 是一位重度爱猫人士,他有一只神奇的魔法猫。
有一天,TT 在 B 站上观看猫猫的比赛。一共有 N 只猫猫,编号依次为1,2,3,…,N进行比赛。比赛结束后,Up 主会为所有的猫猫从前到后依次排名并发放爱吃的小鱼干。不幸的是,此时 TT 的电子设备遭到了宇宙射线的降智打击,一下子都连不上网了,自然也看不到最后的颁奖典礼。
不幸中的万幸,TT 的魔法猫将每场比赛的结果都记录了下来,现在他想编程序确定字典序最小的名次序列,请你帮帮他。

Input

输入有若干组,每组中的第一行为二个数N(1<=N<=500),M;其中N表示猫猫的个数,M表示接着有M行的输入数据。接下来的M行数据中,每行也有两个整数P1,P2表示即编号为 P1 的猫猫赢了编号为 P2 的猫猫。

Output

给出一个符合要求的排名。输出时猫猫的编号之间有空格,最后一名后面没有空格!

其他说明:符合条件的排名可能不是唯一的,此时要求输出时编号小的队伍在前;输入数据保证是正确的,即输入数据确保一定能有一个符合要求的排名。

Sample Input

4 3
1 2
2 3
4 3

Sample Output

1 2 4 3

解题思路

拓扑排序,前向星存图,有向无权。
Kahn算法:
存图后,入度为0的点加入队列,由于编号小的先输出,使用优先队列,设置为小根堆。
每次从堆中取出一个顶点u,加入ans,然后遍历u的所有边(u,v),并删除(即v的in_deg–),如果移除后邻接点v入度为0,将v加入堆中,不断重复上述过程,直到所有点加入ans即可。

代码

#include<iostream>
#include<cstdio>
#include<queue>
#include<vector>

using namespace std;

int n,m,in_deg[510],head[510],tot;
vector<int> ans;
priority_queue<int,vector<int>,greater<int>> q;

struct Edge{
	int to,nxt;
}e[50010];

void add(int u,int v) {
	e[++tot].to=v;
	e[tot].nxt=head[u];
	head[u]=tot;
}
void init() {
	for(int i=0;i<=n;++i) {
		head[i]=-1;
		in_deg[i]=0;
	}
	while(!q.empty()) q.pop();
	ans.clear();
	tot=0;
}

int main() {
	int x=0,y=0;
	while(scanf("%d%d",&n,&m)!=EOF) {
		init();
		while(m--) {
			scanf("%d%d",&x,&y);
			add(x,y);
			in_deg[y]++;
		}
		for(int i=1;i<=n;++i) 
			if(in_deg[i]==0) q.push(i);
		while(!q.empty()) {
			int u=q.top();q.pop();
			ans.push_back(u);
			for(int i=head[u];i!=-1;i=e[i].nxt) {
				int v=e[i].to;
				in_deg[v]--;
				if(in_deg[v]==0) q.push(v);
			}
		}
		for(int i=0;i<ans.size()-1;++i) 
			printf("%d ",ans[i]);
		printf("%d\n",ans[ans.size()-1]);
	}
	return 0;	
}

C-班长竞选

题目描述

大学班级选班长,N 个同学均可以发表意见 若意见为 A B 则表示 A 认为 B 合适,意见具有传递性,即 A 认为 B 合适,B 认为 C 合适,则 A 也认为 C 合适 勤劳的 TT 收集了M条意见,想要知道最高票数,并给出一份候选人名单,即所有得票最多的同学,你能帮帮他吗?

Input

本题有多组数据。第一行 T 表示数据组数。每组数据开始有两个整数 N 和 M (2 <= n <= 5000, 0 <m <= 30000),接下来有 M 行包含两个整数 A 和 B(A != B) 表示 A 认为 B 合适。

Output

对于每组数据,第一行输出 “Case x: ”,x 表示数据的编号,从1开始,紧跟着是最高的票数。 接下来一行输出得票最多的同学的编号,用空格隔开,不忽略行末空格!

Sample Input

2
4 3
3 2
2 0
2 1

3 3
1 0
2 1
0 2

Sample Output

Case 1: 2
0 1
Case 2: 2
0 1 2

解题思路

有向图SCC并缩点,互相可达与单向可达分开考虑。
对于属于第i个SCC的点来说,答案分为两部分(SCC[i]为第i个SCC中点的个数)——

  1. 当前SCC中的点,ans+=SCC[i]-1(去除自己)
  2. 其他SCC中的点,sum(SCC[j]),j可到达i

这种思路下很容易想到,得票最多一定出现在出度为0的SCC中!
那么在反图中,对每个入度为0的点dfs,累加能达到点的SCC[i],即可得到答案。

具体做法:
用Kosaraju求SCC
① 第一遍dfs遍历原图,得到原图的后序序列dfn
② 对后序序列从n开始逆向对所有点第二遍dfs(相当于对逆后序序列从头开始),c[i]中存储原始点i所属的SCC编号,SCC[i]为编号为i的SCC中点的个数

缩点
将同一个SCC的点合并成一个点,遍历反图中的每个点的邻接边,如果边的两端点不属于同一SCC则在缩点图中加入该边,加入边时,计数每个SCC入度,最终形成新的缩点图。

计算每个SCC获得的票数存入ans[i]
由题可知:同一SCC中的点获得票数相同为SCC-1
遍历缩点后的图,对于入度为0的点(缩点后的图源于反图,所以在原图就是出度为0的点,所有可以到达它的点,都给它投票),进行第三遍dfs计算每个缩点总共获得的票数(去除自己)

计算最大得票数max_ans
遍历原图的所有点,如果某点在所在的SCC的ans==max_ans,则输出该点

注意

原题序号为0-N-1,图中为1-N,因此最后应输出i-1。

代码

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>

using namespace std;

int t,n,m,tot1,tot2,tot,dcnt,scnt,sum,max_ans;
int h_org[5010],h_inv[5010],h_ctr[5010],vis[5010];
int dfn[5010],c[5010],scc[5010],in_deg[5010],ans[5010],s_vis[5010];

struct Edge {
	int to,nxt;
}e_org[30010],e_inv[30010],e_ctr[30010];

void add1(int u,int v) {//原图 
	e_org[++tot1].to=v;
	e_org[tot1].nxt=h_org[u];
	h_org[u]=tot1;
}

void add2(int u,int v) {//反图 
	e_inv[++tot2].to=v;
	e_inv[tot2].nxt=h_inv[u];
	h_inv[u]=tot2;
}

void add(int u,int v) {//缩点图 
	e_ctr[++tot].to=v;
	e_ctr[tot].nxt=h_ctr[u];
	h_ctr[u]=tot;
	in_deg[v]++;
}

void dfs1(int x) {
	vis[x]=1;
	for(int i=h_org[x];i;i=e_org[i].nxt) {
		int y=e_org[i].to;
		if(!vis[y]) dfs1(y);
	}
	//后序
	dfn[++dcnt]=x;
} 

void dfs2(int x) {
	c[x]=scnt;
	scc[scnt]++;
	for(int i=h_inv[x];i;i=e_inv[i].nxt) {
		int y=e_inv[i].to;
		if(!c[y]) dfs2(y);
	}
}

void dfs(int x) {
	s_vis[x]=1;
	sum+=scc[x];
	for(int i=h_ctr[x];i;i=e_ctr[i].nxt) {
		int y=e_ctr[i].to;
		if(!s_vis[y]) dfs(y);
	} 
}

void kosaraju() {
	dcnt=0,scnt=0;
	memset(c,0,sizeof c);
	memset(vis,0,sizeof vis);
	//正向遍历 
	for(int i=1;i<=n;++i) 
		if(!vis[i]) dfs1(i);
	//逆后序;
	for(int i=n;i>=1;--i) 
		if(!c[dfn[i]]) ++scnt,dfs2(dfn[i]); 
}

void shrink() {//缩点
	for(int x=1;x<=n;++x) {
		for(int i=h_inv[x];i;i=e_inv[i].nxt) {
			int y=e_inv[i].to;
			if(c[x]==c[y]) continue;
			add(c[x],c[y]);
		}
	}
}

void init() {
	for(int i=0;i<=5010;++i) {
		h_org[i]=0;h_inv[i]=0;h_ctr[i]=0;
		dfn[i]=0;in_deg[i]=0;scc[i]=0;ans[i]=0;
	}
	sum=0;max_ans=0;tot1=0;tot2=0;tot=0;
}
int main() {
	scanf("%d",&t);
	int a=0,b=0;
	for(int k=1;k<=t;++k) {
		scanf("%d%d",&n,&m);
		init();
		for(int i=1;i<=m;++i) {
			scanf("%d%d",&a,&b);
			add1(a+1,b+1);
			add2(b+1,a+1);
		}
		kosaraju();
		shrink();
		for(int i=1;i<=scnt;++i) {
			if(!in_deg[i]) {
				sum=0;
				//每个入度为0的连通分量都要dfs 
				memset(s_vis,0,sizeof s_vis);
				dfs(i);
				ans[i]=sum-1;
				if(max_ans<ans[i]) max_ans=ans[i];
			}
		}
		printf("Case %d: %d\n",k,max_ans);
		int flag=0;
		for(int i=1;i<=n;++i) {
			if(ans[c[i]]==max_ans) {
				if(!flag) {
					printf("%d",i-1);
					flag++;
				}
				else printf(" %d",i-1);
			}
		}
		printf("\n");	
	} 
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值