程序设计思维与实践 Week8 Blog

一、大纲

本周作业与实验题目如下:

  • 区间选点II(差分约束系统)
  • 猫猫向前冲(拓扑排序)
  • 班长竞选(Kosaraju模拟)

二、逐个击破

1.区间选点II(差分约束系统)

题目描述

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

  1. Input

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

  1. Output

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

题目分析

  • 差分约束系统
      • 一种特殊的n 元一次不等式组,它包含n个变量以及m个约束条件。
      • 每个约束条件是由两个其中的变量做差构成的,形如 x i − x j ≤ c k x_i-x_j\leq c_k xixjck ,其中 c k c_k ck 是常数(可以是非负数,也可以是负数)。
      • 我们要解决的问题是:求一组解 x 1 = a 1 x_1=a_1 x1=a1 , x 2 = a 2 x_2=a_2 x2=a2 , … , x n = a n x_n=a_n xn=an ,使得所有的约束条件得到满足,否则判断出无解。

   注意到,如果 a 1 , a 2 , a 3 , … , a n {a_1, a_2, a_3, … , a_n } a1,a2,a3,,an 是该差分约束系统的一组解,那么对
于任意的常数𝑑,显然 a 1 + d , a 2 + d , a 3 + d , … , a n + d {a_1+d, a_2+d, a_3+d, … , a_n +d} a1+d,a2+d,a3+d,,an+d 也是该差分约束系统的一组解,因为这样做差后𝑑 刚好被消掉。

  • 结论
      求解差分约束系统,都可以转化为图论中单源最短路问题, 对于差分约束中的每一个不等式约束 x i − x j ≤ c k x_i-x_j\leq c_k xixjck都可以移项变形为 x i ≤ c k + x j x_i\leq c_k+x_j xick+xj,如果令 c k = w ( i , j ) , d i s [ i ] = x i , d i s [ j ] = x j c_k=w(i,j),dis[i]=x_i,dis[j]=x_j ck=w(i,j),dis[i]=xi,dis[j]=xj,那么原式变为 d i s [ i ] ≤ d i s [ j ] + w ( i , j ) dis[i]\leq dis[j]+w(i,j) dis[i]dis[j]+w(i,j),与最短路问题中的松弛操作很像!

  • 注意
      在求解最短路的时候,程序是按照等于号来计算的,此时跑最短路得到的是一组最大解(上界),如果想要求最小解(下界)则将≤变成≥然后跑最长路即可,原理都是一样的

下面具体分析这道题目如何构造不等式组,记 d i s [ i ] dis[i] dis[i]表示数轴上 [ 0 , i ] [0,i] [0,i]之间的选点个数,对于第i个区间 [ a i , b i ] [a_i,b_i] [ai,bi]需要满足 d i s [ b i ] − d i s [ a i − 1 ] ≥ c i dis[b_i]-dis[a_i-1]\geq c_i dis[bi]dis[ai1]ci,另外还需要保证 0 ≤ d i s [ i ] − d [ i − 1 ] ≤ 1 0\leq dis[i]-d[i-1]\leq 1 0dis[i]d[i1]1,即保证第i个点要么选要么不选,然后求解该差分约束系统的最小解转化为大于等于好求单源最长路,最后的答案为 d i s [ m a x [ b i ] ] dis[max[b_i]] dis[max[bi]]

  然后下面是题目的全部代码:

#include<iostream>
#include<queue>
#include<cstring>
#define inf 1e8
using namespace std;
const int N =  5*1e4+10;
const int M = (5*1e4+10)*3;
int w[N],vis[N],dis[N];
//vis-点在不在队列中
//dis[x]-距离
int head[N],tot,n; 
 

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;
	tot++;
}

void init(int n)
{
	tot=0;
	for(int i=0;i<n;i++)
	{
		head[i]=-1;
		dis[i]=-inf;//一定注意跑最长路这里要赋值为负的 
	 } 
	memset(vis,0,sizeof(vis));
}

queue<int> q;

void spfa(int s)
{
	//队列中加入初始点
	dis[s] = 0,vis[s] = 1;
	q.push(s);
	while(!q.empty()) 
	{
		int x = q.front(); q.pop();
		//x出队列,vis[x]  = 0
		vis[x] = 0;
		//松弛操作
		for(int i=head[x];i!=-1;i = e[i].nxt)
		{
			int y = e[i].to;
			if(dis[y]<dis[x]+e[i].w)
			{//跑最长路问题 
				dis[y] = dis[x] + e[i].w;				
				if(!vis[y])
				{
					vis[y] = 1;
					q.push(y); 
				}
			}			
		 } 
	 } 
}

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

  Tips:由于会出现 d i s [ i − 1 ] dis[i-1] dis[i1],所以防止处理的数据会出现 i = 0 i=0 i=0的情况,所以整体向右移动一格单位进行存储数据!!(这也是附加数据的考点)

2.猫猫向前冲(拓扑排序)

题目描述

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

  1. Input

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

  1. Output

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

题目分析

  拓扑排序有向无环图顶点的线性排序,拓扑排序最常用的算法为Kahn算法。在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,才能称为该图的一个拓扑排序:

  • 序列中包含每一个顶点,且每个顶点只出现一次;
  • 若A在序列中排在B的前面,则在图中不存在从B到A的路径。

原理:

  1. 将入度为0的点组成一个集合S
  2. 每次从S里面取出一个顶点u(可以随便取)放入L , 然后遍历顶点u的
    所有边(u, v) , 并删除之,并判断如果该边的另一个顶点v,如果在移除
    这一条边后入度为0 , 那么就将这个顶点放入集合S中。不断地重复取
    出顶点然后重复这个过程。
  3. 最后当集合为空后,就检查图中是否存在任何边。如果有,那么这个图
    一定有环路,否者返回L , L中顺序就是拓扑排序的结果

伪代码如下图所示:
在这里插入图片描述

  现在具体分析这一道题目,其实本质就是一道单纯的拓扑排序问题,唯一有些不同的是由于答案可能有多组,在输出的时候要求按照字典序最小进行输出,自然我们不可能随即求出一组答案然后进行字典序调整,因为拓扑关系就无法保证了,所以要求我们输出即为所求,那么就应该从数据结构的角度取思考,采取的解决方式是将队列改为使用最小堆,这样就可以保证取队首的时候可以取队列中编号最小的点出队,然后我再描述图的信息的时候使用的是链式前向星。
  注意:priority_queue默认的优先级队列的形式为最大堆,可以将压入的数改为负数就可以变为最小堆,别忘记取出后进行取反!

  综上所述,该题全部解决代码如下:

#include<iostream>
#include<queue>
#include<vector>
using namespace std;
const int N =  505;
const int M = 500*500;
//vis-点在不在队列中
//dis[x]-距离
int head[N],in_deg[N],tot; 
 
struct edge
{
	int to,nxt;
}e[M];

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

void init()
{
	for(int i=0;i<N;i++)
	{
		head[i]=-1;
		in_deg[i]=0;
		tot=0;
	}
}

int main()
{
	int N,M;
	
	while(~scanf("%d%d",&N,&M))
	{
		init();//初始化 
		while(M--)
		{
			int P1,P2;
			scanf("%d%d",&P1,&P2); 
			add(P1,P2);
			in_deg[P2]++;
		}
		priority_queue<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!=-1;i=e[i].nxt)
				if(--in_deg[e[i].to]==0) q.push(-e[i].to);	
		 } 
		 for(int i=0;i<N;i++)
		 {
		 	printf(i==N-1?"%d\n":"%d ",ans[i]);
		 }
		
	}
	return 0;
 } 

3.班长竞选(Kosaraju模拟)

题目描述

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

  1. Input

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

  1. Output

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

题目分析

   强连通分量(SCC)
   强连通分量定义为极大的强连通子图,其中极大的意思是点尽可能的多;如果两个点 A A A B B B是强连通的,说明存在A到B的路径也存在B到A的路径。如下红色方框内为一个SCC,蓝色方向表示SCC之间的连接关系。
在这里插入图片描述
   接下来就是在已知图的基础上如何找出其中的SCC,这就需要用到DFS序的概念,DFS前序表示第一次达到点x 的次序,用 d [ x ] d[x] d[x] 表示;DFS后序表示x 点遍历完成的次序,即回溯时间,用 f [ x ] f[x] f[x] 表示;逆后序是指后序序列的逆序,有了DFS序的概念,下面介绍找出SCC的算法——Kosaraju算法
  Kosaraju算法

  • 第一遍dfs 确定原图逆后序序列
  • 第二遍dfs 在反图中按照逆后序序列进行遍历

反图即将原图中的有向边反向
每次由起点遍历到的点即构成一个SCC

  那么对于这道具体的题目来说,我们发现由于意见存在传递性,所以我们需要找到的就是这个图所对应的强连通分量,然而我们不仅需要知道一个SCC中有几个点,还需要求出SCC之间的传递关系,那么如果我们对于每一个小的结点都求一遍dfs进行求和这样复杂度太高了,所以我们考虑缩点,将一个SCC中的所有点认为是一个“大点”,如果以我们上面列出的图来说明缩点即为如下情况:
在这里插入图片描述
  缩点后,不难发现对于属于第i 个SCC 的点来说,答案分为两部分,令SCC[i] 表示第i 个SCC 中点的个数,当前SCC 中的点, a n s = S C C [ i ] − 1 ans=SCC[i]-1 ans=SCC[i]1,再加上其他SCC中的点 S U M ( S C C [ j ] ) SUM(SCC[j]) SUM(SCC[j]),其中j可以到达i,但是不难发现稍加思考,可以发现最后答案一定出现在出度为0 的SCC中,可以用反证法证明所,以这道题目具体的代码如下:

#include<iostream>
#include<queue>
#include<vector>
#include<cstring>
using namespace std;
const int MAXN =  5005;
const int MAXM = 3*1e4+5;
int head1[MAXN],head2[MAXN],head3[MAXN],tot1,tot2,tot3;
int c[MAXN],dfn[MAXN],vis[MAXN],dcnt,scnt;
//dcnt--dfs序计数,scnt--scc计数 ,c[i]--i号点所在的SCC编号 
int scc[MAXN],in_deg[MAXN];//scc[i]--表示第i个scc中点的个数 
int ans[MAXN];//最后输出的答案 
 
struct edge
{
	int to,nxt;
}e1[MAXM],e2[MAXM],e3[MAXM];//e1为原图,e2为反图,e3为scc点的反图 

void add1(int x,int y)
{	
	e1[tot1].to = y;
	e1[tot1].nxt = head1[x];
	head1[x] = tot1;
	tot1++;
}

void add2(int x,int y)
{	
	e2[tot2].to = y;
	e2[tot2].nxt = head2[x];
	head2[x] = tot2;
	tot2++;
}

void add3(int x,int y)
{
	e3[tot3].to = y;
	e3[tot3].nxt = head3[x];
	head3[x] = tot3;
	tot3++;
}

void init()
{
	tot1=tot2=tot3=0,dcnt=0,scnt=0;
	for(int i=0;i<MAXN;i++)
	{
		head1[i]=-1;
		head2[i]=-1;
		head3[i]=-1;
		c[i]=0;
		dfn[i]=0;
		vis[i]=0;
		scc[i]=0;
		in_deg[i]=0;
		ans[i]=0;
	} 
	for(int i=0;i<MAXM;i++)
	{
		e1[i].nxt=0;
		e1[i].to=0;
		e2[i].nxt=0;
		e2[i].to=0;
		e3[i].nxt=0;
		e3[i].to=0;
	}		
}

void dfs(int x,int m)
{
	vis[x]=1;
	for(int i=head3[x];i!=-1;i=e3[i].nxt) 
	{
		if(!vis[e3[i].to])
		{
			ans[m]+=scc[e3[i].to];
			dfs(e3[i].to,m);
		 }
	}
}

void dfs1(int x)
{
	vis[x]=1;
	for(int i=head1[x];i!=-1;i=e1[i].nxt) if(!vis[e1[i].to]) dfs1(e1[i].to);
	dfn[++dcnt] = x;
}

void dfs2(int x)
{
	c[x]=scnt;
	for(int i=head2[x];i!=-1;i=e2[i].nxt) if(!c[e2[i].to]) dfs2(e2[i].to);
	scc[scnt]++; 
}

void kosaraju(int n)
{
	//初始化
	memset(c,0,sizeof(c));
	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]); 
}

int main()
{
	int T;
	scanf("%d",&T);
	int cc=1;
	while(T--)
	{
		init();
		int N,M;
		scanf("%d%d",&N,&M);
		while(M--)
		{
			int A,B;
			scanf("%d%d",&A,&B);
			add1(A,B);
			add2(B,A);//构建反图 
		}
		vector<int>ps;
		kosaraju(N);
		//缩点 
		for(int i=0;i<N;i++)
			for(int j=head1[i];j!=-1;j=e1[j].nxt)
			{//遍历所有的边
				int y=e1[j].to;
				if(c[i]!=c[y])
				{
					add3(c[y],c[i]);
					in_deg[c[i]]++;	
				}				
			}
		
		
		int max_ans=-1;
		for(int i=1;i<=scnt;i++)
		{
			if(in_deg[i]==0)
			{
				ans[i]+=scc[i]-1;//先加上自己scc中的认同个数 
				memset(vis,0,sizeof(vis));
				dfs(i,i);
			}
			if(max_ans<ans[i])max_ans=ans[i];				
		} 
		printf("Case %d: %d\n",cc,max_ans);
		cc++;
		int out_cnt=0; 
		for(int i=0;i<N;i++)
			if(ans[c[i]]==max_ans)
			{
				if(out_cnt==0)
				{
					printf("%d",i);
					out_cnt++;
				}
				else printf(" %d",i);
			}					
		printf("\n");
	}
	return 0; 
 } 
	
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值