程序设计思维与实践 week7 作业

A-TT的魔法猫

众所周知,TT 有一只魔法猫。

这一天,TT 正在专心致志地玩《猫和老鼠》游戏,然而比赛还没开始,聪明的魔法猫便告诉了 TT 比赛的最终结果。TT 非常诧异,不仅诧异于他的小猫咪居然会说话,更诧异于这可爱的小不点为何有如此魔力?

魔法猫告诉 TT,它其实拥有一张游戏胜负表,上面有 N 个人以及 M 个胜负关系,每个胜负关系为 A B,表示 A 能胜过 B,且胜负关系具有传递性。即 A 胜过 B,B 胜过 C,则 A 也能胜过 C。

TT 不相信他的小猫咪什么比赛都能预测,因此他想知道有多少对选手的胜负无法预先得知,你能帮帮他吗?

Input

第一行给出数据组数。
每组数据第一行给出 N 和 M(N , M <= 500)。
接下来 M 行,每行给出 A B,表示 A 可以胜过 B。

Sample Input

3
3 3
1 2
1 3
2 3
3 2
1 2
2 3
4 2
1 2
3 4

Sample Output

0
0
4

思路

Floyd算法有两个应用,一是求多源最短路,另一个就是求图上的传递闭包,基本思想都是类似的,想知道两个点是否有某种关系,一方面可以判断这两个点是否有直接的关系,另一方面可以判断这两个点是否有间接的关系,即通过某个中间点或中转点使得这两个点建立起某种关系(就是朋友的朋友是朋友),不断遍历这个中间点,就是Floyd算法最外层的循环。
Floyd算法复杂度为O(n3),根据数据规模判断,复杂度可行。
由于胜负关系具有传递性,那么胜关系(偏序关系)可以视为有向边,也就可以使用建立二维数组表示的图。
i 可达 j ,要么 i 有一条有向边直接指向 j ,要么有一个中间点 k , i 可达 k , k 可达 j ,这两个至少满足一个即可。

	str[i][j]=max(str[i][j],str[i][k]&&str[k][j]);

写好裸的Floyd后,没想到超时了。这时需要剪枝了。
剪枝主要就是为了减少进入循环的次数,二维数组,三重循环,剪枝也只可能在第二重循环里剪枝。如果 i 能直接到达 j ,与 k 无关;如果 i 不能直接到达 k ,那么 i 通过 k 还是到达不了 j ,因此如果 i 不能到达 k ,那么i 和 j 还是原来的关系,也就没必要更新,更新了也一样。

		if(!str[i][k]) continue;
		for(int j=1;j<=n;j++){
			str[i][j]=max(str[i][j],str[k][j]);
		}

代码

#include <bits/stdc++.h>
using namespace std;

const int nmax=505;
const int mmax=505;
bool str[nmax][nmax];
int n,m,t;

int func(int n){
	for(int k=1;k<=n;k++){
		for(int i=1;i<=n;i++){
			if(!str[i][k]) continue;
			for(int j=1;j<=n;j++){
				str[i][j]=max(str[i][j],str[k][j]);
			}
		}
	}
	int total=0;

	for(int i=1;i<=n;i++){
		for(int j=i+1;j<=n;j++){
			if(!str[i][j] && !str[j][i]) 
				total++;
		}
	}
	return total;
}

int main(){
	scanf("%d",&t);
	for(int u=0;u<t;u++){
		cin>>n>>m;
		memset(str,false,sizeof str);
		int a,b;
		for(int i=0;i<m;i++){
			cin>>a>>b;
			str[a][b]=true;
		}
//		cout<<"ans:"<<func(n)<<endl;
		cout<<func(n)<<endl;
		
	}
}

B-TT的旅行日记

众所周知,TT 有一只魔法猫。
今天他在 B 站上开启了一次旅行直播,记录他与魔法猫在喵星旅游时的奇遇。 TT 从家里出发,准备乘坐猫猫快线前往喵星机场。猫猫快线分为经济线和商业线两种,它们的速度与价钱都不同。当然啦,商业线要比经济线贵,TT 平常只能坐经济线,但是今天 TT 的魔法猫变出了一张商业线车票,可以坐一站商业线。假设 TT 换乘的时间忽略不计,请你帮 TT 找到一条去喵星机场最快的线路,不然就要误机了!

输入

输入包含多组数据。每组数据第一行为 3 个整数 N, S 和 E (2 ≤ N ≤ 500, 1 ≤ S, E ≤ 100),即猫猫快线中的车站总数,起点和终点(即喵星机场所在站)编号。
下一行包含一个整数 M (1 ≤ M ≤ 1000),即经济线的路段条数。
接下来有 M 行,每行 3 个整数 X, Y, Z (1 ≤ X, Y ≤ N, 1 ≤ Z ≤ 100),表示 TT 可以乘坐经济线在车站 X 和车站 Y 之间往返,其中单程需要 Z 分钟。
下一行为商业线的路段条数 K (1 ≤ K ≤ 1000)。
接下来 K 行是商业线路段的描述,格式同经济线。
所有路段都是双向的,但有可能必须使用商业车票才能到达机场。保证最优解唯一。

输出

对于每组数据,输出3行。第一行按访问顺序给出 TT 经过的各个车站(包括起点和终点),第二行是 TT 换乘商业线的车站编号(如果没有使用商业线车票,输出"Ticket Not Used",不含引号),第三行是 TT 前往喵星机场花费的总时间。
本题不忽略多余的空格和制表符,且每一组答案间要输出一个换行

输入样例

4 1 4
4
1 2 2
1 3 3
2 4 4
3 4 5
1
2 4 3

输出样例

1 2 4
2
5

思路

从起点到终点,可以划分为两种方式:乘坐一次商业线与不乘坐商业线。
对于不乘坐商业线的情况,直接找从起点到终点的最短路即可(在一些特殊情况下,可能是无穷大,必须乘坐商业线)
对于乘坐商业线的情况,基本路程为从起点到商业线的一个车站u,从商业线的另一个车站v再到终点。可以枚举每一条商业线,计算起点到 u 的最短路以及 v 到终点的最短路再加上商业线的时间。
即对于一天商业线(u , v , w ),计算商业线的方向不同时的用时,取min { dis1[u]+dis2[v]+w , dis1[v]+dis2[u]+w } ,这是一条商业线的最短用时,枚举每一条商业线,取用时最短的一条商业线,最后再与不走商业线的用时相比取最小值,这是基本的思路。
在记录经济线的边时,使用了链式前向星的数据结构,而使用vector数组记录商业线的边便于遍历,由于边都是无向边,一条边需要正反添加两次。
在计算最短路时,使用了dijkstra算法,很模板化。分别从起点和终点各开始最短路的长度和路径的前驱。
在计算出乘坐一次商业线的最短用时之后,需要再与不乘坐商业线相比较一下,有不同的路径遍历方法。
考虑到路径遍历中数组记录的是前驱,我一开始使用了stack与deque来遍历的同时记录路径

		if(wmin > dis1[e]){//不需要商业线 
//			cout<<"不需要商业线路"<<endl;
			int temp=e;
			stack<int> route; while(route.size()) route.pop();
			while(temp!=-1){
				route.push(temp);
				temp=pre1[temp];
			}
			while(route.size() != 1 ){
				cout<<route.top()<<" ";
				route.pop();
			}
			cout<<e<<endl;
			cout<<"Ticked Not Used"<<endl;
			cout<<dis1[e]<<endl;
}
		else{//需要商业线 
//			cout<<"需要使用商业线路"<<endl;
			int from=line[index].from;
			int to=line[index].to;
			int w1=dis1[line[index].from]+dis2[line[index].to]+line[index].time;
			int w2=dis1[line[index].to]+dis2[line[index].from]+line[index].time;
			
			if(w2<w1) swap(from,to);
			deque<int> route;route.clear();//			
			int temp=to;
			while(temp!=-1){
				route.push_back(temp);
				temp=pre2[temp];
			}
			temp=from;
			while(temp!=-1){
				route.push_front(temp);
				temp=pre1[temp];
			}
			while(route.size() != 1){
				cout<<route.front()<<" ";
				route.pop_front();
			}
			cout<<e<<endl;
			cout<<from<<endl;
			cout<<wmin<<endl;
}

然而这样复杂,繁琐,极易容易出错,实际上一开始我就在这里错了好几次,使用一些样例来测很难发现错误,陷入了误区。
实际上,使用并查集的思想和方法很容易解决

void dfs(int x){
	if(x == s){cout<<x;return ;}
	dfs(pre1[x]);
	cout<<" "<<x;
}
void output(int x){
	while(x != e){
		cout<<" "<<x;
		x=pre2[x];
	}
	cout<<" "<<x;
}

还有在乘坐商业线时注意记录方向,如果是反的话没必要再分开处理

		int from=line[index].from;
		int to=line[index].to;
		int w1=dis1[line[index].from]+dis2[line[index].to]+line[index].time;
		int w2=dis1[line[index].to]+dis2[line[index].from]+line[index].time;
		if(w2<w1) swap(from,to);

这个题目还有一种分层图的方法,如果商业线不止一条的话上面的方法就不太好了。

代码

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

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

const int nmax=505;
int inf=1e8;
int dis1[nmax],dis2[nmax];
bool vis[nmax];
int pre1[nmax],pre2[nmax];

const int mmax=2005;
int head[mmax];
Edge edge[mmax];
int cnt=0;

vector<Edge> line;
int n,s,e,m,k,flag=0;

void addedge(int from,int to,int time){
	edge[cnt].from=from;
	edge[cnt].to=to;
	edge[cnt].time=time;
	edge[cnt].next=head[from];
	head[from]=cnt++;
}
void init(){
	for(int i=0;i<mmax;i++) head[i]=-1;
	cnt=0;
}


priority_queue<pair<int,int>> q;

void dijkstra(int s,int n,int *dis,int *pre){
	while(q.size()) q.pop();
	memset(vis,0,sizeof(bool)*nmax);
	memset(pre,-1,sizeof(int)*nmax);
	for(int i=1;i<=n;i++) dis[i]=inf;
	dis[s]=0;
	
	q.push(make_pair(0,s));
	while(q.size()){
		int x=q.top().second;q.pop();
		if(vis[x]) continue;
		vis[x]=1;
		for(int i=head[x];i!=-1;i=edge[i].next){
			int y=edge[i].to;
			int w=edge[i].time;
			if(dis[y] > dis[x]+w){
				dis[y]=dis[x]+w;
				pre[y]=edge[i].from;
				q.push(make_pair(-dis[y],y));
			}
		}
	}
}

void dfs(int x){
	if(x == s){cout<<x;return ;}
	dfs(pre1[x]);
	cout<<" "<<x;
}
void output(int x){
	while(x != e){
		cout<<" "<<x;
		x=pre2[x];
	}
	cout<<" "<<x;
}

int main(){
	
	while(~scanf("%d%d%d",&n,&s,&e)){
		memset(dis1,0,sizeof dis1);
		memset(dis2,0,sizeof dis2);
		memset(pre1,-1,sizeof pre1);
		memset(pre2,-1,sizeof pre2);
		memset(vis,0,sizeof vis);
		memset(head,-1,sizeof head);
		line.clear();
		
		
		scanf("%d",&m);init();
		for(int i=0;i<m;i++){
			int from,to,time;scanf("%d%d%d",&from,&to,&time);
			addedge(from,to,time);addedge(to,from,time);
		}

		dijkstra(s,n,dis1,pre1);
		dijkstra(e,n,dis2,pre2);
		
		scanf("%d",&k);
		for(int i=0;i<k;i++){
			int from,to,time;
			scanf("%d%d%d",&from,&to,&time);
			line.push_back(Edge(from,to,time));line.push_back(Edge(from,to,time));
		}
		
		int index=0,wmin=inf;
		for(int i=0;i<line.size();i++){
			int w=min(dis1[line[i].from]+dis2[line[i].to]+line[i].time,dis1[line[i].to]+dis2[line[i].from]+line[i].time);
			if(wmin>w){wmin=w;index=i;}
		}
				
		if(flag>0) cout<<endl;
		flag++;
				
		if(wmin > dis1[e]){//不需要商业线 
			dfs(e);
			cout<<endl<<"Ticket Not Used"<<endl;
			cout<<dis1[e]<<endl;

		}
		else{//需要商业线 
			int from=line[index].from;
			int to=line[index].to;
			int w1=dis1[line[index].from]+dis2[line[index].to]+line[index].time;
			int w2=dis1[line[index].to]+dis2[line[index].from]+line[index].time;
			if(w2<w1) swap(from,to);

			dfs(from);
			output(to);
			cout<<endl<<from<<endl;
			cout<<wmin<<endl;
			
		}
//		cout<<endl;
	}
	return 0;
}

C-TT的美梦

这一晚,TT 做了个美梦!

在梦中,TT 的愿望成真了,他成为了喵星的统领!喵星上有 N 个商业城市,编号 1 ~ N,其中 1 号城市是 TT 所在的城市,即首都。

喵星上共有 M 条有向道路供商业城市相互往来。但是随着喵星商业的日渐繁荣,有些道路变得非常拥挤。正在 TT 为之苦恼之时,他的魔法小猫咪提出了一个解决方案!TT 欣然接受并针对该方案颁布了一项新的政策。

具体政策如下:对每一个商业城市标记一个正整数,表示其繁荣程度,当每一只喵沿道路从一个商业城市走到另一个商业城市时,TT 都会收取它们(目的地繁荣程度 - 出发地繁荣程度)^ 3 的税。

TT 打算测试一下这项政策是否合理,因此他想知道从首都出发,走到其他城市至少要交多少的税,如果总金额小于 3 或者无法到达请悄咪咪地打出 ‘?’。

Input

第一行输入 T,表明共有 T 组数据。(1 <= T <= 50)

对于每一组数据,第一行输入 N,表示点的个数。(1 <= N <= 200)

第二行输入 N 个整数,表示 1 ~ N 点的权值 a[i]。(0 <= a[i] <= 20)

第三行输入 M,表示有向道路的条数。(0 <= M <= 100000)

接下来 M 行,每行有两个整数 A B,表示存在一条 A 到 B 的有向道路。

接下来给出一个整数 Q,表示询问个数。(0 <= Q <= 100000)

每一次询问给出一个 P,表示求 1 号点到 P 号点的最少税费。

Output

每个询问输出一行,如果不可达或税费小于 3 则输出 ‘?’。

Sample Input

2
5
6 7 8 9 10
6
1 2
2 3
3 4
1 5
5 4
4 5
2
4
5
10
1 2 4 4 5 6 7 8 9 10
10
1 2
2 3
3 1
1 4
4 5
5 6
6 7
7 8
8 9
9 10
2
3 10

Sample Output

Case 1:
3
4
Case 2:
?
?

思路

这个题目很明显是一个单源最短路问题,求起点到其他点的最小耗费。相关的算法有dijkstra算法,Bellman-Ford算法SPFA算法等,由于很可能存在负权边,而且这个图点少边多(边数最多可能是点的完全图的边数的1/4),使用Bellman-Ford算法复杂度接近O(n3),因此使用SPFA算法。
这里使用链式前向星来记录边,在找点的邻接边的时候很方便,链式前向星里没必要存from,但是这里为了输出调试的时候看着方便就加上了。

int ind;
int head[mmax];
Edge edge[mmax];
void init(){
	memset(arr,0, sizeof arr);
	memset(ques,0,sizeof ques);
	memset(head,-1,sizeof head);
	memset(flag,0,sizeof flag); 
	ind=1;
}
void addedge(int from,int to,int weight){
	edge[ind].from=from;
	edge[ind].to=to;
	edge[ind].weight=weight;
	edge[ind].next=head[from];
	head[from]=ind++;
}

问题的关键就是SPFA算法了。SPFA从一个点开始松弛临界边的方法类似于bfs的遍历方法,使用一个队列,相当于按照深度一层一层的来进行松弛,代码与bfs遍历方法很相似,其中不同数组有不同的作用:
dis数组:记录起点到图中各个点的距离,初始化为无穷大
pre数组:记录遍历过程中点的前驱节点
inq数组:记录一个点是否在队列中(in queue),如果遍历到这个点已经在队列中,就不需要重复添加了
cnt数组:记录层数,即这是第几次松弛操作了,当松弛次数>=点数时,就说明它是绕着一个环来回转,而且点在松弛时到起点的距离不断减小才能一直转,这就说明出现了负环。

void spfa(int n,int s){//n是点的个数,s是起始位置
    for(int i=0;i<=n;i++){
        dis[i]=inf;
        pre[i]=0;
        inq[i]=0;
        cnt[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 to=edge[i].to;
			if(dis[to] > dis[u]+edge[i].weight){
				cnt[to]=cnt[u]+1;
				if(cnt[to] >= n ){//找到负环,从to开始遍历标记 
					dfs(to);
//					cout<<"负环开始:"<<to<<endl;
					continue;
				}
				dis[to]=dis[u]+edge[i].weight;
				pre[to]=u;
				if(!inq[to]){
					q.push(to);
					inq[to]=1;
				}
			}
		}
	}
}

如果找到了一个负环的点,那么从这个点开始可以继续通向其他的点,本来负环上的点到起点的距离就一直减小到负无穷,那么这个点所能到达的点 到 起点的距离也就是负无穷了。也就是说,负环上的一个点所能到达的点到起点的距离都是负无穷了,那么从这个点开始遍历据可以了,bfs或dfs。

void dfs(int u){
	if(flag[u]) return ;
	flag[u]=1;
	for(int i=head[u];i!=-1;i=edge[i].next){
		int v=edge[i].to;
		dfs(v);
	}
}

前面使用了flag数组来表示一个点是否被负环所影响,使用dis数组来记录起点到其他点的距离,flag[u] == false || dis[u] < 3 即可判断是总金额小于3 或是 无法到达(由于被负环所影响),这时千万别忘了一种最基本的无法到达:不连通 ,我就是一开始忘了这个情况 。判断这个情况很容易,既然不连通,无法到达,那么到起点的距离就是无穷大了,而且由于与起点相联通的图中也没有点可以到达,它就不会更新 , 直接判断 dis[u]==inf 就可以了。

	if(flag[ques[i]] || dis[ques[i]]<3 || dis[ques[i]]==inf)
		printf("?\n");

代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <queue>
using namespace std;

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

int inf=1e8;
int t,n,m,question;
const int nmax=205;
const int mmax=1e5+5;
int arr[nmax],ques[mmax];
bool flag[nmax];//标记点是否在负环上 

int dis[mmax],pre[mmax],inq[mmax],cnt[mmax];
queue<int> q;

int ind;
int head[mmax];
Edge edge[mmax];
void init(){
	memset(arr,0, sizeof arr);
	memset(ques,0,sizeof ques);
	memset(head,-1,sizeof head);
	memset(flag,0,sizeof flag); 
	ind=1;
}
void addedge(int from,int to,int weight){
	edge[ind].from=from;
	edge[ind].to=to;
	edge[ind].weight=weight;
	edge[ind].next=head[from];
	head[from]=ind++;
}

void dfs(int u){
	if(flag[u]) return ;
	flag[u]=1;
	for(int i=head[u];i!=-1;i=edge[i].next){
		int v=edge[i].to;
		dfs(v);
	}
}

void spfa(int n,int s){//n是点的个数,s是起始位置
    for(int i=0;i<=n;i++){
        dis[i]=inf;
        pre[i]=0;
        inq[i]=0;
        cnt[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 to=edge[i].to;
			if(dis[to] > dis[u]+edge[i].weight){
				cnt[to]=cnt[u]+1;
				if(cnt[to] >= n ){//找到负环,从to开始遍历标记 
					dfs(to);
//					cout<<"负环开始:"<<to<<endl;
					continue;
				}
				dis[to]=dis[u]+edge[i].weight;
				pre[to]=u;
				if(!inq[to]){
					q.push(to);
					inq[to]=1;
				}
			}
		}
	}
}

int main(){
	scanf("%d",&t);
	for(int u=1;u<=t;u++){
		init();
		
		scanf("%d",&n);
		for(int i=1;i<=n;i++)	scanf("%d",&arr[i]);
		scanf("%d",&m);
		for(int i=1;i<=m;i++){
			int a,b; scanf("%d %d",&a,&b);
			addedge(a,b,pow(arr[b]-arr[a],3));
		}
		scanf("%d",&question);
		for(int i=0;i<question;i++) scanf("%d",&ques[i]);
		
		spfa(n,1);
		
		printf("Case %d:\n",u);
		for(int i=0;i<question;i++){
			if(flag[ques[i]] || dis[ques[i]]<3 || dis[ques[i]]==inf)
				printf("?\n");
			else
				printf("%d\n",dis[ques[i]]);
		}

	}
} 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值