SDU week8 作业(A 区间选点II B猫猫向前冲 C班长竞选)

A:区间选点——(差分约束与spfa)

题目:

给定一个数轴上的 n 个区间,要求在数轴上选取最少的点使得第 i 个区间 [ai, bi] 里至少有 ci 个点

使用差分约束系统的解法解决这道题

输入:

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

输出:

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

样例:

样例输入:

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

样例输出:

6

思路:

差分约束系统

  • 一种特殊的n元一次不等式组,它包含n个变量以及m个约束条件。
  • 每种约束条件是由其中的变量做差构成的,形如Xi-Xj<Ck,其中Ck是常数
  • 我们要解决的问题是:求一组解x1=a1,x2=a2,…xn=an,使得所有的约束条件得到满足,否则判断出无解。

求解差分约束系统,都可以转化为图论中得单源最短路问题
对于差分约束中的每一个不等式约束Xi-Xj<Ck,都可以移项变形为Xi<=Ck+Xj.如果令Ck=w(i,j) dis[i]=Xi,dis[j]=Xj ,那么原式变为dis[i]<=dis[j]+w(i,j),与最短路中的松弛操作相似。
由于原式为Xi<=Ck+Xj,求解是按照等于求解,跑最短路出来的最大解,如果想要最小解即为Xi>=Ck+Xj,跑最长路。

构造不等式组:
记sum[i]表示数轴上[0,i]之间选点的个数
对于第i个区间[ai,bi]需要满足sum[bi]-sum[ai-1]>=ci
同时需要保证sum[i]是有意义的,
0<=sum[i]-sum[i-1]<=1;
这时就会出现0<=sum[1]-sum[0]<=1; 此时为了方便整体后移改成sum[bi+1]-sum[ai]>=ci ; (可以后移也是基于差分约束系统+d,-d结果不变)

代码:

#include<stdio.h>
#include<queue>
#include<iostream>
#include<string.h>
using namespace std;
#define maxn 50010
#define inf  1e9
struct edge{
	int u,v,w,next;
};
edge E[5000100];
int head[maxn],tot;
int dis[maxn];
bool vis[maxn];
int n,r;
void add(int u,int v,int w)
{
	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;
	while(!q.empty()) q.pop();
	for(int i=0;i<=r;i++){
		dis[i]=-inf;
		vis[i]=0;
	}
	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;
				}
			}
		}
	}
} 
int main()
{
	cin>>n;
	int a,b,c;
	tot=0;
	memset(head,0,sizeof(head));
	for(int i=0;i<n;i++)
	{
		cin>>a>>b>>c;
		add(a,b+1,c);
		r=max(r,b+1);
	}
	for(int i=1;i<=r;i++){
		add(i-1,i,0);
		add(i,i-1,-1);
	}
	spfa(0);
	printf("%d\n",dis[r]);
	return 0;
}

B:猫猫向前冲——(拓扑排序)

题目:

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

输入:

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

输出:

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

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

样例:

样例输入:

4 3
1 2
2 3
4 3

样例输出:

1 2 4 3

思路:

这个分析题意比较容易看出来是一个拓扑排序的题目。
拓扑排序的基本步骤:
从入度为0的点加入到队列中,选取队列中的一个点,将 u 加入结果数组ans,并且 u 出发的线去掉,对应的v,Ru[u]–,同时将入度为0的v 加入到队列中;当ans 中的点是n 的时候,所有点加入完毕,输出结果。
如果是一般的拓扑排序用队列实现,结果可以有很多种,但是现在要求字典序最小,因此不能用一般的队列,而应该用优先级队列 。

总结:

这个题的思路是比较明确的,需要注意的是,这里数据结构的选取。
这里需要用到的就是获得从u->v 的边,这里用链式前向星就不是很合适了,找边的时候增加了困难。可以用的数据结构可以是vector 存储的二维数组,静态数组,链式存储。这里选用的比较方便的静态数组。

代码:

#include<stdio.h>
#include<queue>
#include<vector>
#include<iostream>
#include<string.h>
using namespace std;
#define maxn 550
int cat[maxn][maxn];
int R[maxn];  //记录入度 
int n; 
int vis[maxn]; 
void kahn()
{
	priority_queue<int,vector<int>,greater<int>> q;
	vector<int> ans; 
	while(!q.empty())	q.pop();
	for(int i=1;i<=n;i++)
	{
		if(R[i]==0){
			q.push(i);	
		}
	}
	while(!q.empty()){
		int u=q.top() ; //去掉以u 为起始点的边,边对应的终点度数-- 
		q.pop();
		ans.push_back(u);
		if(ans.size()==n){
			for(int i=0;i<n;i++){
				if(i!=n-1)	printf("%d ",ans[i]);
				else	printf("%d",ans[i]);
			}
		}
		for(int i=1;i<=n;i++){
			if(cat[u][i]==1){
				R[i]--;
				if(R[i]<=0){
					q.push(i);
				}
			}
		} 
	}
}
int main()
{
	int m,a,b;
	while(scanf("%d%d",&n,&m)!=EOF){
		memset(cat,0,sizeof(cat));
		memset(R,0,sizeof(R));
		while(m--){
			cin>>a>>b;
			if(!cat[a][b]){
				cat[a][b]=1;    //a 打败 b 
				R[b]++;
			}
		}
		kahn();
		printf("\n");
	}
} 

C:班长竞选——(SCC 与缩点)

题目:

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

输入:

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

输出:

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

样例:

样例输入:

2
4 3
3 2
2 0
2 1

3 3
1 0
2 1
0 2

样例输出:

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

思路:

这个题 的目标就是求出最高的票数和获得最高票数的人;
在一个强连通图中每个人获得的票数的就是连通图中的点-1(自身),其次就要考虑其他图连向该图的点。
这样分析后思路也会比较清晰:
(1)用Kosaraju 求出scc
(2)缩点,一个SCC作为一个点,连接SCC之间的线,得图G。此时存反图 fG。遍历点得到,每个 SCC中的详细的点。遍历边,然后·寻找跨越不同SCC 中的边。(需要注意的是,SCC之间的边可能重复,因此需要用vector 中的函数进行去重)
(3)经分析可以知道,最大的票数应该在G 中指向SCC 最多的,但是这样计算不方便,因此用反图fG, 从出度为0的SCC 遍历,获取此SCC所能到达的所有SCC ,统计票数。

总结:

总的体验就是步骤繁琐,但是好在每一步都比较清晰,debug 时每一步能达到的效果都是知道的,比较容易找错。这次给人的感受是数据结构根据需要选用。

查找最后结果有两种情况,SCC内点和其他SCC 中的点,但是这两种情况不是每个SCC 都存在的,因此,计算的时候也需要分成两部分,用an 记录其他SCC 指向该SCC 的点,本身SCC 中的点必然存在。

Koasaraju算法步骤:
(1)第一遍dfs 确定原图的逆后序序列
(2)第二遍dfs 在反图中按照逆后序序列进行遍历(每次由起点遍历到的点即构成一个SCC)
Koasaraju获得重要的内容:
(1)c[i]:顶点 i 所在的 SCC
(2)scnt :SCC的总数目
Koasaraju 模板代码:

void dfs1(int u)
{
	vis[u]=1;
	for(int i=0;i<a[u].size();i++){
		if(!vis[a[u][i]]){
			dfs1(a[u][i]);
		}
	}
	dfn[dcnt]=u;  //后序序列 
	dcnt++;
 } 
void dfs2(int u)
{
	c[u]=scnt;     //c[i] 顶点i 所在的scc编号  scnt 总scc的数量 
	for(int i=0;i<b[u].size();i++){
		if(!c[b[u][i]]){
			dfs2(b[u][i]);
		}
	}
} 
void kosaraju()
{
	dcnt=scnt=0;
	memset(c,0,sizeof(c));
	memset(vis,0,sizeof(vis));
	memset(dfn,-1,sizeof(dfn));
	for(int i=0;i<n;i++){
		if(!vis[i])	dfs1(i);  
	}
	for(int i=n-1;i>=0;i--){  //按照逆后序序列遍历
		if(!c[dfn[i]]){  //当前点还不在任意一个SCC
			++scnt;     //出现新的SCC scnt++
			dfs2(dfn[i]);
		}
	} 
} 

代码:

#include<stdio.h>
#include<vector>
#include<string.h>
#include<algorithm>
#include<iostream>
using namespace std;
#define N 5010
vector<int> a[N],b[N],G[N],G1[N]; 
int n,c[N],vis[N],dfn[N],dcnt,scnt,Ru[N],sum[N],anss[N];
int an;
struct edge{
	int u,v;
};
edge E[30010];  //边集
void dfs1(int u)
{
	vis[u]=1;
	for(int i=0;i<a[u].size();i++){
		if(!vis[a[u][i]]){
			dfs1(a[u][i]);
		}
	}
	dfn[dcnt]=u;  //后序序列 
	dcnt++;
 } 
void dfs2(int u)
{
	c[u]=scnt;     //c[i] 顶点i 所在的scc编号  scnt 总scc的数量 
	for(int i=0;i<b[u].size();i++){
		if(!c[b[u][i]]){
			dfs2(b[u][i]);
		}
	}
} 
void kosaraju()
{
	dcnt=scnt=0;
	memset(c,0,sizeof(c));
	memset(vis,0,sizeof(vis));
	memset(dfn,-1,sizeof(dfn));
	for(int i=0;i<n;i++){
		if(!vis[i])	dfs1(i);  // 
	}
	for(int i=n-1;i>=0;i--){
		if(!c[dfn[i]]){
			++scnt;
			dfs2(dfn[i]);
		}
	} 
} 
void dfs3(int u)  //所有u 能到达的点 
{
	vis[u]=1;
	//an=G1[u].size();  //G1 SCC 内点   G缩点后的图 G[u][i]  从u能到达的点 
	for(int i=0;i<G[u].size();i++){ 
		int v=G[u][i];
		if(!vis[v]) {
			vis[v]=1;
			an=an+G1[v].size();
		//	cout<<v<<" "<<G1[v].size()<<" "<<an<<endl;
			dfs3(v);
		}
	}
}
int main()
{
	int T,m,A,B;
	scanf("%d",&T);
	for(int kk=1;kk<=T;kk++)
	{
		scanf("%d%d",&n,&m);
		for(int i=0;i<=n+1;i++)
		{
			a[i].clear();
			b[i].clear();
			G1[i].clear();
			G[i].clear();
		}
		int k=0; 
		for(int i=0;i<m;i++){
			scanf("%d%d",&A,&B); 
			a[A].push_back(B);  //原图 
			b[B].push_back(A);    //反图 
			E[k].u=A;
			E[k].v=B;
			k++;  
		}
		kosaraju();  //求SCC 
		//记录每个scc中的点的个数
		for(int i=0;i<n;i++)
		{
			G1[c[i]].push_back(i);   //第i 个点在scc  
		} 
		//cout<<G1[1].size()<<endl;
		//遍历所有边,得scc 的新图 
		memset(Ru,0,sizeof(Ru));
		for(int i=0;i<m;i++){
			if(c[E[i].u]!=c[E[i].v]){   //连接两个scc的边   //加入到新图中  c[i] 是顶点, 
				G[c[E[i].v]].push_back(c[E[i].u]);  //缩点后的反图 
				Ru[c[E[i].u]]++;
			}
		}
		/*
		cout<<"scnt : "<<scnt<<endl;
		cout<<"反图 :" <<endl;
		for(int i=1;i<=scnt;i++){
			cout<<"sc: "<<i<<" ";
			for(int j=0;j<G[i].size();j++){
				cout<<G[i][j]<<" ";  //
			}
			cout<<endl;
		} 
		cout<<"SCC内点"<<endl; 
		for(int i=1;i<=scnt;i++){
			cout<<"scn: "<<i<<": ";
			for(int j=0;j<G1[i].size();j++){
				cout<<G1[i][j]<<" ";
			}
			cout<<endl;
		} */
		//去重,可能有相同的边
		for(int i=1;i<=scnt;i++) 
		{
		 	sort(G[i].begin(),G[i].end());
		 	G[i].erase(unique(G[i].begin(),G[i].end()),G[i].end());
		}
		// 求入度为0的新图的dfs
		memset(sum,0,sizeof(sum));
		bool flag=false;
		for(int i=1;i<=scnt;i++)
		{
			an=0;
			if(Ru[i]==0)   //如果不存在入度为0的点 
			{
				memset(vis,0,sizeof(vis));   //dfs 错误 
				dfs3(i);
			}
			sum[i]=an+G1[i].size()-1;
		 }
		 //求出最大值 所在scc-1 + dfs()
		 int cnt,ans=0; 
		 for(int i=1;i<=scnt;i++)
		 {
		 //	cout<<ans<<" "<<sum[i]<<endl;
		 	if(ans<sum[i])
		 	{
		 		ans=sum[i];  //最高的票数 
			}
		}
		//求拥有最高票数的同学
		k=0;
		memset(anss,0,sizeof(anss));
		 for(int i=1;i<=scnt;i++)
		 {
		 	if(sum[i]==ans)
			{
		 		//scc中所有的点都需要加入
				for(int j=0;j<G1[i].size();j++)
				{
					anss[k]=G1[i][j];
					k++;
				}
		 	}
		} 
		sort(anss,anss+k);
		//输出:最高的票数+同学编号  
		printf("Case %d: %d\n",kk,ans);
		for(int i=0;i<k-1;i++){
			printf("%d ",anss[i]);
		}
		printf("%d\n",anss[k-1]);
 	} 
 	return 0;
 }
发布了13 篇原创文章 · 获赞 0 · 访问量 194

猜你喜欢

转载自blog.csdn.net/weixin_45736432/article/details/105511214
今日推荐