Week8 作业

A - 区间选点 II

题目

给定一个数轴上的 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

解析

类似的题曾经在前面通过贪心的做法实现过,不过这里要求要使用差分约束的方法。

所谓差分约束,就是通过一组固定形式的不等式组来约束变量间的关系,其泛型为:𝑥1 − 𝑥2 ≤ 𝑐 ,c为常数。这类似于解不等式组操作。

对于泛型的不等式约束,我们通过移项,可以将其变为 𝑥1 ≤ 𝑐 + 𝑥2, 将c视为图中(链式前向星描述)的 w(i, j),x1视为dis [i] , x2视为dis [j],原式变为了 𝑑𝑖𝑠[𝑖] ≤ 𝑑𝑖𝑠[𝑗] + 𝑤(𝑖, 𝑗),与最短路的松弛操作类似。这样,我们把xi视作图中一个节点,对每个约束,视为从节点 i 到节点 j 连了一条长度为c的有向边(注意这里是有向边!),这样问题就变成了上周讲过的最短路问题。

当图中存在负环时(本题),用SPFA算法解决。

对本题,令sum [i] 为数轴上[ 0, i ] 之间选点的个数,其每个区间 [ ai, bi ] 满足约束条件如下:
𝑠𝑢𝑚[𝑏i] − 𝑠𝑢𝑚[𝑎i − 1] ≥ 𝑐i

转化成泛型,跑一遍最长路即可。

代码

#include<iostream>
#include<string.h>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int n = 50005;
const int m = 1000005;
const int inf = 5e8;

int N = 0, tot = 0, ans = 0;
int head[n], inq[n], dis[n];
queue<int> q;

struct edge{
	int to, next, w;
}edges[m];

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


void SPFA(int s){
	for(int i = 1 ; i <= ans ; ++i)
	{
		dis[i] = -inf;
		inq[i] = 0;
	}
	dis[s] = 0;
	inq[s] = 1;
	q.push(s);
	while(!q.empty())
	{
		int u = q.front();
		q.pop();
		inq[u] = 0;
		for(int i = head[u] ; i != 0 ; i = edges[i].next)
		{
			int v = edges[i].to;
			if(dis[v] < dis[u] + edges[i].w)
			{
				dis[v] = dis[u] + edges[i].w;
				if(!inq[v])
				{
					q.push(v);
					inq[v] = 1;
				} 
			}
		}
	}
}

int main(){
	int N;
	cin >> N;
	for(int i = 0 ; i < N ; i++)
	{
		int u, v, w;
		cin >> u >> v >> w;
		add(u, v + 1, w);
		ans = max(ans, v + 1);
	}
	for(int i = 1 ; i <= ans ; i++)
	{
		add(i - 1, i, 0);
		add(i, i - 1, -1);
	}
	SPFA(0);
	cout << dis[ans] << endl;
	return 0;
}

回顾

这题刚开始补充数据超时了,原因是直接套了上次C题的板子,好多没用的数组初始化没有删掉,事实告诉我们不要偷懒…

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

解析

拓扑排序问题。本题实质上就是给图中的节点(猫咪)排序。

拓扑排序的方法与求最小生成树的方法很相似,都是通过队列来实现。因为拓扑排序后的起点一定入度为0(可以通过反证法证明),所以先挑出虽有入度为零的点,将其入队,之后依次出队,遍历出队元素可到达的点,然后判断这些点入度减一,为0且未到达过则入队。重复这个过程至队列空,即可得到结果。

注意拓扑排序的结果不唯一。

代码

#include<iostream>
#include<string.h>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int N = 50005;
const int M = 250005;

int tot = 0;
int head[N], inq[N], dis[N];
priority_queue<int, vector<int>, greater<int> > q;
vector<int> a;

int in_deg[510];


struct edge{
	int to, next, w;
}edges[M];

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

void toposort(int n)
{
	a.clear();
	while(!q.empty())
		q.pop();
	
	for(int i = 1 ; i <= n ; i++)
		if(in_deg[i] == 0)	
			q.push(i);

	while(!q.empty())
	{
		int u = q.top();
		q.pop();
		a.push_back(u);
		for(int i = head[u] ; i != 0 ; i = edges[i].next)
		{
			int v = edges[i].to;
			if(--in_deg[v] == 0)
				q.push(v);
		} 
	}

}

int main()
{
	int n, m;
	while(cin >> n >> m)
	{
		tot = 0; 
		for(int i = 0 ; i <= n ; i++)
		{
			head[i] = 0;
			in_deg[i] = 0;
		}	
		
		for(int i = 0 ; i < m ; i++)
		{
			int p1, p2;
			cin >> p1 >> p2;
			add(p1, p2, 1);
			in_deg[p2]++;
		}
		
		toposort(n);
		
		for(int i = 0 ; i < a.size() - 1 ; i++)
			cout << a[i] << " ";
		cout << a[a.size()-1] << endl;
	}
}

回顾

拓扑排序因为有之前的算法相印证,所以相对好理解。结合PPT中给出的代码,很容易就解决了这个问题。

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(强连通分量)中的人的支持。如此,我们把图分成几个连通分量。

寻找连通分量,我们通过Kosaraju算法来解决。算法的步骤为,第一遍dfs1确定原图的逆后序序列;之后第二遍dfs2,根据逆后序序列依次遍历每个点,将到达的点标记,每次由起点遍历到的点即为一个联通分量(vis数组标记过的已到达的点将不再计算),所属的SCC由c数组记录。算法成立的原理是当图中的所有边反向时,强连通分量不受影响。

上面说到,如果一个人得到了另一个人的支持,那么它也一定得到了和和那个人处在同一个SCC(强连通分量)中的人的支持。那么,我们把每个强连通分量替换成一个点,将强连通分量间的连通关系视为点与点之间的连通关系。如此操作后,对于每个点,其支持人数分为 两部分,(令 SCC[i] 表示第 i 个 SCC 中点的个数),第一部分为当前 SCC 中的点,ans += SCC[i] – 1(去除自己),第二部分为其它 SCC 中的点 SUM ( SCC[j] ),其中 j 可到达 i。

如此,我们便求得了答案。

代码

#include<iostream>
#include<string.h>
#include<algorithm>
#include<cstring>
#include<queue>
#include<vector>
using namespace std;
vector<int> G1[5005], G2[5005], G3[5005];
int n, c[5005], dfn[5005], vis[5005], dcnt, scnt, indeg[5005];

struct point 
{
	vector<int> v;
}P[5005];

void dfs1(int x)
{
	vis[x] = 1;
	for(int i = 0 ; i < G1[x].size() ; i++)
		if(!vis[G1[x][i]])
			dfs1(G1[x][i]);
	dfn[dcnt++] = x;
}

void dfs2(int x)
{
	c[x] = scnt;
	for(int i = 0 ; i < G2[x].size() ; i++)
		if(!c[G2[x][i]])
			dfs2(G2[x][i]);
}

int dfs3(int x)
{
	vis[x] = 1;
	int y = P[x].v.size();
	for(int i = 0 ; i < G3[x].size() ; i++)
		if(!vis[G3[x][i]])
			y += dfs3(G3[x][i]);
	return y;
}

void kosaraju()
{
	dcnt = scnt = 0;
	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 - 1 ; i >= 0; i--)
		if(!c[dfn[i]])
		{
			++scnt;
			dfs2(dfn[i]);
		}
}

void suodian(){
	for(int i =1 ; i <= scnt ; i++)
	{
		G3[i].clear();
		P[i].v.clear();
		indeg[i] = 0;
	}		 
	for(int i = 0 ; i < n ; i++)
	{
		P[c[i]].v.push_back(i);
		for(int j = 0 ; j < G1[i].size() ; j++)
		{
			
			if(c[i] == c[G1[i][j]])
				continue;
			else 
			{
				G3[c[G1[i][j]]].push_back(c[i]);
				indeg[c[i]]++;
			}
		}
	}
}

void SET(){
	for(int i = 1 ; i <= scnt ; i++)
	{
		sort(G3[i].begin(), G3[i].end());
		G3[i].erase(unique(G3[i].begin(), G3[i].end()), G3[i].end());
	}
}

int tag = 1;

int main()
{
	ios::sync_with_stdio(false);
	int T;
	cin >> T;
	while(T--)
	{
		int m;
		cin >> n >> m;
		for(int i = 0 ; i < n ; i++)
		{
			G1[i].clear();
			G2[i].clear();
		}
		while(m--)
		{
			int a, b;
			cin >> a >> b;
			G1[a].push_back(b);
			G2[b].push_back(a);
		}
		//求SCC
		kosaraju();
		//缩点
		suodian();
		//去重 
		SET();
		
		
		int sum[scnt + 1] = {0};
		int ans[5005] = {0};
		
		int Max = 0;
		for(int i = 1 ; i <= scnt ; i++)
		{
			if(!indeg[i])
			{
				for(int j = 1 ; j <= scnt ; j++)
					vis[j] = 0;
				sum[i] = dfs3(i) - 1;
				if(Max < sum[i])	
					Max = sum[i];
			}
		}
		
		int num = 0;
		for(int i = 1 ; i <= scnt ; i++)
		{
			if(sum[i] == Max)
			{
				for(int j = 0 ; j < P[i].v.size() ; j++)
				{
					ans[num] = P[i].v[j];
					num++;
				}
			}
		}
		sort(ans, ans + num); 
		cout << "Case " << tag << ": " << Max << endl;
		tag++;
		for(int i = 1 ; i < num ; i++)
			cout << ans[i - 1] << " ";
		cout << ans[num - 1] << endl;
	}
}

回顾

这道题下课后听了一遍回放还是能理解的,可是实际操作时代码部分难度挺大的,瑟瑟发抖。虽然理解了缩点的操作,但是操作起来还是出了很多问题,最后十分艰难才解决。菜鸡瑟瑟发抖。

另外这是第一次因为没有加 ios::sync_with_stdio(false) 而导致超时的。这个点学长在第一节课说到过,但是没太在意,没想到它在某些情况下对时间的影响如此之大。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值