程序设计思维与实践 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

思路

使用差分约束系统首先需要构造不等式。题意为在区间 [ a i , b i ] [a_i, b_i] [ai,bi]中至少有 c i c_i ci个点,这里用到了前缀和将区间转换为了差分形式,从开头到 b i b_i bi 大区间的点数与开头到 a i − 1 a_i-1 ai1小区间的点数之差即为区间 [ a i , b i ] [a_i , b_i] [ai,bi]中的点数( s u m [ i ] sum[i] sum[i]表示数轴上 [ 0 , i ] [0,i] [0,i]之间选点的个数)
s u m [ b i ] − s u m [ a i − 1 ] ≤ c   i   sum[ b_i] - sum[ a_i-1 ] \le c~i~ sum[bi]sum[ai1]c i 
同时,区间内的一个点有选与不选两种情况,同样使用前缀和转换为差分的形式为
0 ≤ s u m [ i ] − s u m [ i − 1 ] ≤ 1 0 \le sum[ i ] - sum[ i-1 ] \le 1 0sum[i]sum[i1]1
求解最少选取的点数,也就是求解差分约束系统的最小解,对上面的两个不等式进行移项,得到
s u m [ b i ] ≥ s u m [ a i − 1 ] + c i sum[b_i] \ge sum[a_i-1] + c_i sum[bi]sum[ai1]+ci
s u m [ i ] ≥ s u m [ i − 1 ] + 0 sum[i] \ge sum[i-1] + 0 sum[i]sum[i1]+0
s u m [ i − 1 ] ≥ s u m [ i ] + ( − 1 ) sum[i-1] \ge sum[i] + (-1) sum[i1]sum[i]+(1)
转换成了标准差分约束系统的形式
对于标准差分约束系统的一个不等式 a − b ≥ k a-b\ge k abk,移项为 a ≥ b + k a\ge b+k ab+k,可以理解为原点到a的距离大于原点到b的距离与 b b b a a a的距离之和,也就是相当于存在一条从 b b b a a a的长度为 k k k的有向边,对于每一个与 a a a相邻的 b b b, d i s [ a ] ≥ d i s [ b ] + w ( b , a ) dis[a]\ge dis[b]+w(b,a) dis[a]dis[b]+w(b,a) 根据spfa的松弛操作,每次大于等于都是取等于,因此保证了最后 d i s [ a ] dis[a] dis[a]是满足要求的最小的(如果在某次松弛操作中给 d i s [ a ] dis[a] dis[a]多加一点也是满足要求的,但明显每次取等于是最小的),这样松弛也相当于不断拉长 a a a与原点的距离,也就是取最长路。
最后找到所有区间中端点最靠右的右端点 b b b s u m [ b ] sum[b] sum[b]就是所有区间最少选的点的个数。
由于仅仅需要 d i s [ b ] dis[b] dis[b],原来spfa中所需的一些数组可以取消,需要注意的是,跑最长路时dis数组需要初始化为负无穷大,距离慢慢的变长,否则dis数组一直不变

void spfa(int s){
	for(int i=0;i<=n;i++){
		dis[i]=-inf;//最长路这里初始化为负无穷大 
		inq[i]=0;
	}
	while(q.size()) q.pop();
	dis[s]=0;inq[s]=1;
	q.push(s);
	while(q.size()){
		int u=q.front();q.pop();
		inq[u]=0;
		for(int i=head[u];i!=-1;i=edge[i].next){
			int v=edge[i].to;
			if(dis[v]<dis[u]+edge[i].weight){
				dis[v]=dis[u]+edge[i].weight;
				if(!inq[v]){
					q.push(v);
					inq[v]=1;
				}
			}
		}
	}
}

这里存储图使用了链式前向星的方式,需要注意的是链式前向星中的边不仅仅是输入的从a-1到b的边,还有相邻的两个点之间的边

	for(int i=0;i<n;i++){
		int a,b,c;
		scanf("%d%d%d",&a,&b,&c);
		maxb=max(maxb,b);
		addedge(a-1,b,c);
	}
	for(int i=1;i<=maxb;i++){
		addedge(i-1,i,0);
		addedge(i,i-1,-1);
	}

更容易被忽略的是链式前向星的边的条数不再是n,还要加上每相邻两个点之间的两条边,在定义数组的时候要注意
同时需要注意的是dis数组应该为long long 型的,如果是int型的可能溢出

const int nmax=50005;
Edge edge[nmax*3];//为了方便就直接乘3了 
int head[nmax];//点的个数不变 

代码

#include <iostream>
#include <stdio.h>
#include <string.h>
#include <queue>
using namespace std;

struct Edge{
	int from,to,next;
	long long weight;
	Edge(){from=-1;to=-1;weight=0;next=-1;}
	Edge(int _from,int _to,long long _weight){
		from=_from;
		to=_to;
		weight=_weight;
		next=-1;
	}
	void show(){
		cout<<"from:"<<from<<"  to:"<<to<<"  weight:"<<weight<<"  next:"<<next<<endl;
	}
};

const int nmax=50010;
const long long int inf=1e12;
Edge edge[nmax*4];
int head[nmax];
int ind,n;
void init(){
//	for(int i=0;i<=3*n;i++) head[i]=-1;
	memset(head,-1,sizeof head);
	ind=0;
}
void addedge(int from,int to,long long weight){
	edge[ind].from=from;
	edge[ind].to=to;
	edge[ind].weight=weight;
	edge[ind].next=head[from];
	head[from]=ind++;
}

int inq[nmax];
long long dis[nmax];
queue<int> q;

void spfa(int s){
	for(int i=0;i<=n;i++){
		dis[i]=-inf;//最长路这里初始化为负无穷大 
		inq[i]=0;
	}
	while(q.size()) q.pop();
	dis[s]=0;
	inq[s]=1;
	q.push(s);
	while(q.size()){
		int u=q.front();q.pop();
		inq[u]=0;
		for(int i=head[u];i!=-1;i=edge[i].next){
			int v=edge[i].to;
			if(dis[v]<dis[u]+edge[i].weight){
				dis[v]=dis[u]+edge[i].weight;
				if(!inq[v]){
					q.push(v);
					inq[v]=1;
				}
			}
		}
	}
}

int main(){
	scanf("%d",&n);
	int maxb=-1,mina=1e5;
	init();
	for(int i=0;i<n;i++){
		int a,b;
		long long c;
		scanf("%d%d%lld",&a,&b,&c);
		addedge(a,b+1,c);
		maxb=max(maxb,b+1);
		mina=min(mina,a);
	}
	for(int i=mina;i<maxb;i++){
		addedge(i,i+1,0);
		addedge(i+1,i,-1);
	}

	spfa(mina);
	cout<<dis[maxb]<<endl; 

	
	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 3 4

思路

上周有一道题目与这道题的题意类似,只不过那道题目是求取闭包,得出任意两个点的连通关系,而这道题目是求拓扑排序,并且题目保证这个图是连通的。
拓扑排序很类似与bfs,从图中向队列中添加入度为0的点(入度的数组在添加边的同时建立),从队列中不断取出一个点,删除这个点相邻接(指向)的边,不断更新与这个点相邻接的点的入度,并且将入度为0的点再加入队列,过程与bfs只是在入队条件那里有些区别。如果要求出字典序最小的拓扑序,将队列换成优先队列(小根堆),保证每次从队列中弹出的是当前编号最小的点。

bool toposort(){
	for(int i=1;i<=n;i++){
		if(indeg[i]==0) q.push(i);
	}
	
	while(q.size()){
		int temp=q.top();q.pop();
		s.push_back(temp);
		
		for(int i=head[temp];i!=-1;i=edge[i].next){
			indeg[edge[i].to]--;
			if(indeg[edge[i].to]==0)
				q.push(edge[i].to);
		}
		head[temp]=-1;
	}
	return s.size()==n;
}

存储边同样使用了链式前向星,删除边时只需将head中的指针置为-1即表示删除。

代码

#include <iostream>
#include <stdio.h>
#include <string.h>
#include <queue>
#include <vector>
using namespace std;

struct Edge{
	int from,to,next;
	Edge(){from=-1;to=-1;next=-1;}
	Edge(int _from,int _to){
		from=_from;
		to=_to;
		next=0;
	}
	void show(){
		cout<<"from:"<<from<<"  to:"<<to<<"  next:"<<next<<endl;
	}
};

const int nmax=500;
const int lenMax=nmax*nmax+5;
Edge edge[lenMax];
int head[lenMax],indeg[lenMax];
int cnt,m,n;
priority_queue<int,vector<int>,greater<int>> q;
vector<int> s;
queue<int> t;

void init(){
	cnt=0;s.clear();
	for(int i=0;i<lenMax;i++) head[i]=-1;
	while(q.size()) q.pop();
}
void addedge(int from,int to){
	edge[cnt].from=from;
	edge[cnt].to=to;
	edge[cnt].next=head[from];
	head[from]=cnt++;
	indeg[to]++;
}
void deledge(int from){
	indeg[edge[head[from]].to]--;
	head[from]=-1;
}

bool toposort(){
	for(int i=1;i<=n;i++){
		if(indeg[i]==0) q.push(i);
	}
	
	while(q.size()){
		int temp=q.top();q.pop();
		s.push_back(temp);
		
		for(int i=head[temp];i!=-1;i=edge[i].next){
			indeg[edge[i].to]--;
			if(indeg[edge[i].to]==0)
				q.push(edge[i].to);
		}
		head[temp]=-1;
	}
	return s.size()==n;
}

int main(){
	while(~scanf("%d%d",&n,&m)){
		init();
		for(int i=0;i<m;i++){
			int a,b;
			scanf("%d%d",&a,&b);
			addedge(a,b);
		}
		
		if(toposort()){
			for(int i=0;i<n-1;i++)
				cout<<s.at(i)<<" ";
			cout<<s.at(n-1)<<endl;
		}
	}
	return 0;
}

C-班长竞选

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

Input

本题有多组数据。第一行T表示数据组数。每组数据开始有两个整数N和M( 2 ≤ n ≤ 5000 2\le n \le 5000 2n5000 0 < m ≤ 30000 0 < m \le 30000 0<m30000),接下来有M行包含两个整数A和B( A ≠ B A \not =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中,因此首先要找到图中的SCC。
kosaraju算法
使用kosaraju算法,第一个dfs确定原图的逆后序序列,第二次dfs在反图中按照逆后序序列进行遍历,对不同的SCC进行染色。

void dfs1(int x){
	vis[x]=true;
	for(int i=0;i<g1[x].size();i++){
		int temp=g1[x][i];
		if(!vis[temp]) dfs1(temp);
	}
	d[dcnt++]=x;
}
void dfs2(int x){
	c[x]=scnt;num[scnt]++;
	for(int i=0;i<g2[x].size();i++){
		int temp=g2[x][i];
		if(!c[temp]) dfs2(temp);
	}
}
void kosaraju(){
	for(int i=0;i<n;i++){
		if(!vis[i]) dfs1(i);
	}
	for(int i=n-1;i>=0;i--){
		if(!c[d[i]]) ++scnt,dfs2(d[i]);
	}
}

接下来对反图中的SCC进行缩点(原图和反图的SCC相同),将同一种染色的SCC看作是一个节点,重新建图

	for(int i=0;i<n;i++){
		for(int j=0;j<g2[i].size();j++){
			int temp=g2[i][j];
			if(c[i]==c[temp]) continue;//在同一个SCC中 
			g3[c[i]].push_back(c[temp]);
			indeg[c[temp]]++;
		}
	}

缩点后,对于第i个SCC中的点来说,答案分为两个部分,一方面来自于自己SCC中的其他点,另一方面来自于其他可以到达第i个SCC的SCC,因此没有出边的SCC可能是最后的答案,通过对反图缩点重新建图,这样就把入边改变成了出边,将没有出边变成了没有入边,对入度为0的SCC进行dfs或bfs即可
这是大体思路,但是在具体的过程中有许多细节要注意
首先要注意同学的编号是从0开始的,也就是在dfs求逆后序序列时要注意索引的自增与赋值的顺序(先赋值,后自增)

void dfs1(int x){
	vis[x]=true;
	for(int i=0;i<g1[x].size();i++){
		int temp=g1[x][i];
		if(!vis[temp]) dfs1(temp);
	}
	d[dcnt++]=x;
}

其次在记录同学的编号时,使用小根堆来记录比较方便。
最后,原图和反图是可以用链式前向星来存储的,但是缩点图比较适合用vector的邻接链表来存储,点和边相对都少,而且是动态的,这时使用前向星就会占用较多空间。

代码

#include <iostream>
#include <stdio.h>
#include <vector>
#include <queue>
#include <string.h>
#include <algorithm>
using namespace std;

const int nmax=5005;
const int mmax=3e4+5;

int t,n,m,a,b;

vector<int> g1[nmax],g2[nmax],g3[nmax];
int d[nmax],c[nmax],indeg[nmax],num[nmax];
bool vis[nmax];
int dcnt,scnt,ans;
queue<int> q;
priority_queue<int,vector<int>,greater<int>> p;

void init(){
	dcnt=0;scnt=0;ans=0;
	memset(d,0,sizeof d);
	memset(c,0,sizeof c);
	memset(indeg,0,sizeof indeg);
	memset(num,0,sizeof num);
	memset(vis,false,sizeof vis);
	for(int i=0;i<nmax;i++){
		g1[i].clear();
		g2[i].clear();
		g3[i].clear();
	}
}

void dfs1(int x){
	vis[x]=true;
	for(int i=0;i<g1[x].size();i++){
		int temp=g1[x][i];
		if(!vis[temp]) dfs1(temp);
	}
	d[dcnt++]=x;
}
void dfs2(int x){
	c[x]=scnt;num[scnt]++;
	for(int i=0;i<g2[x].size();i++){
		int temp=g2[x][i];
		if(!c[temp]) dfs2(temp);
	}
}
void kosaraju(){
	for(int i=0;i<n;i++){
		if(!vis[i]) dfs1(i);
	}
	for(int i=n-1;i>=0;i--){
		if(!c[d[i]]) ++scnt,dfs2(d[i]);
	}
}

int bfs(int x){
	memset(vis,false,sizeof vis);
	int result=num[x];
	queue<int> qu;
	while(qu.size()) qu.pop();
	qu.push(x);vis[x]=true;
	while(qu.size()){
		int i=qu.front();qu.pop();
		for(int j=0;j<g3[i].size();j++){
			if(vis[g3[i][j]]) continue;
			qu.push(g3[i][j]);vis[g3[i][j]]=true;
			result+=num[g3[i][j]];
		}
	}
	return result;
}


int main(){
	scanf("%d",&t);
	for(int u=1;u<=t;u++){
		init();
		scanf("%d%d",&n,&m);
		for(int i=0;i<m;i++){
			scanf("%d%d",&a,&b);
			g1[a].push_back(b);
			g2[b].push_back(a);
		}
		
		kosaraju();
		
		for(int i=0;i<n;i++){
			for(int j=0;j<g2[i].size();j++){
				int temp=g2[i][j];
				if(c[i]==c[temp]) continue;//在同一个SCC中 
				g3[c[i]].push_back(c[temp]);
				indeg[c[temp]]++;
			}
		}
		
		while(q.size()) q.pop();
		for(int i=1;i<=scnt;i++){
			if(indeg[i]==0) q.push(i);
		}
		while(p.size()) p.pop();
		
		int result=0;
		vector<int> s;
		while(q.size()){
			int cluster=q.front();q.pop();
			int temp=bfs(cluster);
			if(result<temp){
				s.clear();
				s.push_back(cluster);
				result=temp;
			}
			else if(result==temp)
				s.push_back(cluster);
		}//s中存储了集合的编号 
				
		while(p.size()) p.pop();
		for(int i=0;i<s.size();i++){
			int temp=s.at(i);
//			cout<<"temp:"<<temp<<endl;
			for(int j=0;j<n;j++){
				if(c[j]==temp)
					p.push(j);
			}
		}
		printf("Case %d: %d\n",u,result-1);
		while(p.size()!=1){
			cout<<p.top()<<" ";p.pop();
		}
		cout<<p.top()<<endl;p.pop();
	}	
}		
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值