Week8-图和树的性质与应用下

A - 区间选点 II

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

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

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

思路:
  • 首先我们需要将问题转化为一个差分约束系统。由于问题限定a b区间中点的个数至少为c ,联想到不等式的形式,可以有sum[b] - sum[a] >= c那么需要约定这里sum[x]的含义:[0,x]中选取点的个数。可以想见最终的答案应该是sum[max(bi)]。如此根据输入便可以得到一组不等式。
  • 仅仅以上不等式组是不够的:考虑每个点或者选或者不选,因此有0<=sum[i]-sum[i-1]<=1
  • 由于需要取最少的点,因此最终需要得到最小解,因此我们需要跑最长路(这一点考虑到松弛条件如果大于等于那么取等于,从而为最小解)另外,由于存在负边,我们需要使用SPFA。
  • 另外需要注意数据的范围:左边界可以取到0,意味着有sum[0]-sum[-1]的存在,并且sum[-1]=0。这也应该成为跑SPFA的起点。但是显然-1没办法映射到下标,因此需要将区间整体向右平移一个单位,最终答案不会发生变化,而且此时sum[0]=0,从0开始跑SPFA。
实现:
#include<iostream>
#include<stdio.h>
#include <queue>
#include <vector>
#include<string.h>
using namespace std;
const int inf = 1e8; 
const int N = 50010;
const int M = 200000; 

int n, dis[N];
bool vis[N];

int head[N], tot;
struct Edge{
	int to, next, w;
}e[M];

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

void spfa(int s) {
	std::queue<int> q;
	for(int i = 0; i <= 50005; ++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!=-1; i = e[i].next) {
			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()
{
	tot=0;
	int a,b,c,r;
	r = -1;
	cin>>n;
	memset(head,-1,sizeof(head));
	for(int i=0;i<n;++i)
	{
		scanf("%d%d%d",&a,&b,&c);
		r=max(b,r);
		add_edge(a,b+1,c);
	}
	r++;
	for(int i=1;i<=r;++i)
	{
		add_edge(i-1,i,0);
		add_edge(i,i-1,-1);
	}
	spfa(0);
	cout<<dis[r];
	return 0;
}

反思:
  • 错误1: 没有关注边的数量。尽管区间右边界限制了点的个数,但是边的个数需要另外计算,简单的排列组合问题,粗略估算大概是50000 * 50000 * 1/2。
  • 错误2: 直接套用模板,没有细看代码。给出的最长路模板中,对于dis vis数组的初始化是从0到n,但是在main函数中n变成了区间的个数,显然不匹配,需要修改。

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 的猫猫。

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

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

思路:
  • 由于我们知道每场比赛的结果,因此可以得到一个有向图。计算名次序列等价于进行拓扑排序。需要注意的是要求字典序最小的名次序列,因此我们需要使用优先队列:同样入度为0的点,我们优先取出编号较小的点。
  • 图的存储:考虑到我们需要计算入度,因此直接使用vector+结构体进行存储即可。
  • 拓扑排序的算法:
    • 遍历图中各点,计算其入度。对于入度为0的点,加入优先级队列。
    • 当队列不为空时,挑出队首元素,将其加入答案队列。遍历其能到达的点,其入度减1。若为0,则加入队列。
    • 由于保证数据正确,因此不需要判断最后的点的数目。
实现:
#include<iostream>
#include<vector>
#include<queue>
#include<algorithm>
#include<string.h>
using namespace std;

const int MAXN = 510;
struct edge
{
	int u,v;
};
vector<edge> G[MAXN];
void add_edge(int u,int v)
{
	G[u].push_back({u,v});
} 

int N,M,P1,P2;
int in_deg[MAXN]={0};
vector<int> ans;
bool toposort()
{
	for(int i=1;i<=N;++i)
	{
		vector<edge>::iterator ia= G[i].begin();
		while(ia!=G[i].end())
		{
			in_deg[(*ia).v]++;
			ia++;
		}
	}
	
	priority_queue<int, vector<int>,greater<int> > q; 
	for(int i=1;i<=N;++i)
	if(in_deg[i]==0)q.push(i);
	while(!q.empty())
	{
		int x = q.top();
		q.pop();
		//cout<<"TEST: "<<x<<endl;
		ans.push_back(x);
		vector<edge>::iterator ia = G[x].begin();
		while(ia!=G[x].end())
		{
			in_deg[(*ia).v]--;			
			if(in_deg[(*ia).v]==0)q.push((*ia).v);
			ia++;
		}
	}
	return true;
	
}
int main()
{
	ios::sync_with_stdio(false);
	while(cin>>N>>M)
	{
		for(int i=0;i<M;++i)
		{
			cin>>P1>>P2;
			add_edge(P1,P2);
		}
		
		ans.clear();
		memset(in_deg,0,sizeof in_deg);
		
		toposort();
		vector<int>::iterator ia=ans.begin();
		if(ia!=ans.end())
		{
			cout<<*ia;
			ia++;
		}
		while(ia!=ans.end())
		{
			cout<<" "<<*ia;
			ia++;
		}
		cout<<endl;
		
		for(int i=1;i<=N;++i)G[i].clear();
	}
	
	return 0;
}

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开始,紧跟着是最高的票数。接下来一行输出得票最多的同学的编号,用空格隔开,不忽略行末空格!

思路:
  • 由于是有向无权图,因此直接使用vector<int>存储即可。注意需要建立两个图,原图和反图。

  • 利用Kosaraju算法得到所有的SCC。此时的信息包括:每个点所属的SCC。据此进行缩点。为了后续的遍历,我们考虑计算每个SCC所包含的点的个数。遍历一遍即可。

  • 注意在缩点的过程中需要将边反向。由于我们最终的答案存在于出度为0的SCC,因此反向之后寻找入度为0的点,从该点开始进行DFS,从而其到达的SCC即为答案的一部分。(缩点得到了一个新的图,此时图中可能有重复的边,不过并没有影响,因为DFS过程中会进行vis的判断,并且入度为0那么不存在入边。)为此我们考虑需要一个数组,下标表示SCC的编号,内容表示其中点的答案。初始值即为包含点的数目-1。

  • 在完成DFS之后,我们得到了所有可能为答案的SCC对应答案,因此挑出其中的max,之后根据SCC编号输出其中的点的编号。注意可能会有多个SCC答案均为max,需要全部输出。

  • 注意每个点的答案分成两部分:

    • 其所属的SCC中点的数目-1
    • 另外的SUM(SCCi中点的数目),其中SCCi可以到达该SCC。

    因此最后答案一定存在于出度为0的SCC中。(否则,假设某SCC出度不为0,答案在该SCC中,那么其到达的SCC中点的答案一定大于它,矛盾)

实现:
#include<iostream>
#include<vector>
#include<string.h>
using namespace std; 

const int N = 5010;
vector<int> G1[N],G2[N],G3[N],res[N];

int n,c[N],dfn[N],vis[N],dcnt,scnt;
//c -- 表示某点的SCC编号
//dfn -- DFS后序列中的点
//dcnt -- DFS序计数  scnt -- SCC计数

void dfs1(int x)
{
	vis[x] = 1;
	for(vector<int>::iterator y=G1[x].begin();y!=G1[x].end();++y)
		if(!vis[*y])dfs1(*y);
	dfn[dcnt++] = x;
} 
 
void dfs2(int x)
{
	c[x] = scnt;
	for(vector<int>::iterator y=G2[x].begin();y!=G2[x].end();++y)
	if(!c[*y])dfs2(*y);
}

void kosaraju()
{
	dcnt = scnt = 0;
	memset(c,0,sizeof(c));
	memset(vis,0,sizeof(vis));
	
	for(int i=0;i<n;++i)//注意点的编号从0开始 
	if(!vis[i])dfs1(i);
	
	for(int i=n-1;i>=0;--i)
	if(!c[dfn[i]])
	{
		++scnt;
		dfs2(dfn[i]);
	}
}

int num_point[N],in_deg[N],ans_scc[N];//每个SCC包含点的个数 
int start,ans;
void dfs3(int x)
{
	vis[x]=1;
	for(vector<int>::iterator y=G3[x].begin();y!=G3[x].end();++y)
	{
		if(!vis[*y])
		{
			ans_scc[start]+=num_point[*y];
			dfs3(*y);
		}
	}
}
void connect()
{
	for(int i=0;i<n;++i)
	{
		num_point[c[i]]++;
	}	
	
	for(int i=0;i<n;++i)
	{
		for(vector<int>::iterator y=G1[i].begin();y!=G1[i].end();++y)
		{
			if(c[i] != c[*y])
			{
				G3[c[*y]].push_back(c[i]);
				in_deg[c[i]]++;
			}
		}
	}
	memset(vis,0,sizeof(vis));
	for(int i=1;i<=scnt;++i)
	{//此处i表示SCC的编号 
		ans_scc[i]=num_point[i]-1;
		if(in_deg[i]==0)
		{
			start=i;
			dfs3(start); 
			memset(vis,0,sizeof(vis));
		}
	}
}


void init()
{
	for(int i=0;i<n;++i)
	{
		G1[i].clear();
		G2[i].clear();
		G3[i].clear();
	}
	memset(num_point,0,sizeof(num_point)); 
	memset(in_deg,0,sizeof(in_deg));
	memset(ans_scc,0,sizeof(ans_scc));
}

void out(int x)
{	
	int temp=0;
	ans=0;
	for(int i=1;i<=scnt;++i)
	{//考虑到scnt从1开始 
		if(ans_scc[i] > temp)
		{//是否存在多个相同的答案 
			temp = ans_scc[i];
			ans = i;
		}
	}
	int r[N];
	int index=0;
	for(int i=0;i<n;++i)
		if(ans_scc[c[i]]==ans_scc[ans])
			r[index++]=i;

	cout<<"Case "<<x<<": "<<ans_scc[ans]<<endl;
	for(int i=0;i<index-1;++i)
	cout<<r[i]<<" ";
	cout<<r[index-1]<<endl;
}
int main()
{
	ios::sync_with_stdio(false);
	int T,M,A,B;
	cin>>T;
	for(int i=1;i<=T;++i)
	{
		cin>>n>>M;
		init()
		for(int j=1;j<=M;++j)
		{
			cin>>A>>B;
			G1[A].push_back(B);
			G2[B].push_back(A);
		}
		kosaraju();
		connect();
		out(i);
	}
}
反思:
  • 错误1:dfs3过程中应该是+= num_point[*y]而非 num_point[c[*y]]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值