SCNU-Algorithms-MS-2023-贪心算法

贪心算法

1-1旅游规划

题目

有了一张自驾旅游路线图,你会知道城市间的高速公路长度、以及该公路要收取的过路费。现在需要你写一个程序,帮助前来咨询的游客找一条出发地和目的地之间的最短路径。如果有若干条路径都是最短的,那么需要输出最便宜的一条路径。

输入格式:
输入说明:输入数据的第1行给出4个正整数N、M、S、D,其中N(2≤N≤500)是城市的个数,顺便假设城市的编号为0~(N−1);M是高速公路的条数;S是出发地的城市编号;D是目的地的城市编号。随后的M行中,每行给出一条高速公路的信息,分别是:城市1、城市2、高速公路长度、收费额,中间用空格分开,数字均为整数且不超过500。输入保证解的存在。

输出格式:
在一行里输出路径的长度和收费总额,数字间以空格分隔,输出结尾不能有多余空格。

输入样例:

4 5 0 3
0 1 1 20
1 3 2 30
0 3 4 10
0 2 2 20
2 3 1 20

输出样例:

3 40

主要使用Floyd算法,求得起始点和终点的最短路径。该题Floyd算法主要步骤如下:

void Floyd(int N) {
	//CODE
	for (int i = 0; i < N; i++) {
		for (int j = 0; j < N; j++) {
			for (int n = 0; n < N; n++) {
				if (Path_long[i][j] > Path_long[i][n] + Path_long[n][j]) {
					Path_long[i][j] = Path_long[i][n] + Path_long[n][j];
					Path_long[j][i] = Path_long[i][n] + Path_long[n][j];
					Cost[i][j] = Cost[i][n] + Cost[n][j];
					Cost[j][i] = Cost[i][n] + Cost[n][j];
				}
				else if(Path_long[i][j] == Path_long[i][n] + Path_long[n][j]) {
					Cost[i][j] = min(Cost[i][j], Cost[i][n] + Cost[n][j]); 
					Cost[j][i] = min(Cost[i][j], Cost[i][n] + Cost[n][j]);
				}
			}
		}
	}
}

主要是三层嵌套循环,前两层循环主要是遍历了所有Path_long结点。其中,Path_long[ i ][ j ]表示城市 i 到城市 j 的距离(若走不通则为999),针对每个Path_long[ i ][ j ],找任意一个城市n,如果从 i 到 n 再到 j 的路途,是比 i 直接到 j 要短的,那么需要更新该结点,更新值为当前更小的,即 i 到 n 再到 j 的路途。

Path_long[i][j] = Path_long[i][n] + Path_long[n][j];
					Path_long[j][i] = Path_long[i][n] + Path_long[n][j];

同时,如果 i 到 j 和 从 i 到 n 再到 j 的路途是一样的,那么根据开销选择,谁的开销更小,选择哪条路。

else if(Path_long[i][j] == Path_long[i][n] + Path_long[n][j]) {
					Cost[i][j] = min(Cost[i][j], Cost[i][n] + Cost[n][j]); 
					Cost[j][i] = min(Cost[i][j], Cost[i][n] + Cost[n][j]);
				}

因此,完整代码如下:

#include<iostream>

int Path[501][501];
int Path_long[501][501];
int Cost[501][501];

int min(int v1, int v2) {
	if (v1 > v2)
		return v2;
	else
		return v1;
}


void Floyd(int N) {
	//CODE
	for (int i = 0; i < N; i++) {
		for (int j = 0; j < N; j++) {
			for (int n = 0; n < N; n++) {
				if (Path_long[i][j] > Path_long[i][n] + Path_long[n][j]) {
					Path_long[i][j] = Path_long[i][n] + Path_long[n][j];
					Path_long[j][i] = Path_long[i][n] + Path_long[n][j];
					Cost[i][j] = Cost[i][n] + Cost[n][j];
					Cost[j][i] = Cost[i][n] + Cost[n][j];
				}
				else if(Path_long[i][j] == Path_long[i][n] + Path_long[n][j]) {
					Cost[i][j] = min(Cost[i][j], Cost[i][n] + Cost[n][j]); 
					Cost[j][i] = min(Cost[i][j], Cost[i][n] + Cost[n][j]);
				}
			}
		}
	}
}

int main() {

	int N, M, S, D;
	std::cin >> N >> M >> S >> D;
	for (int i = 0; i < N; i++) {
		for (int j = 0; j < N; j++) {
			Path_long[i][j] = 999;
			Cost[i][j] = 999;
		}
	}
	for (int i = 0; i < M; i++) {
		int city1, city2, path_long, cost;
		std::cin >> city1 >> city2 >> path_long >> cost;
		Path_long[city1][city2] = path_long;
		Path_long[city2][city1] = path_long;
		Cost[city1][city2] = cost;
		Cost[city2][city1] = cost;
	}

	Floyd(N);
	std::cout << Path_long[S][D] << " " << Cost[S][D];
}

1-2 拯救007(升级版)

题目

在老电影“007之生死关头”(Live and Let Die)中有一个情节,007被毒贩抓到一个鳄鱼池中心的小岛上,他用了一种极为大胆的方法逃脱 —— 直接踩着池子里一系列鳄鱼的大脑袋跳上岸去!(据说当年替身演员被最后一条鳄鱼咬住了脚,幸好穿的是特别加厚的靴子才逃过一劫。)

设鳄鱼池是长宽为100米的方形,中心坐标为 (0, 0),且东北角坐标为 (50, 50)。池心岛是以 (0, 0) 为圆心、直径15米的圆。给定池中分布的鳄鱼的坐标、以及007一次能跳跃的最大距离,你需要给他指一条最短的逃生路径 —— 所谓“最短”是指007要跳跃的步数最少。

输入格式:

首先第一行给出两个正整数:鳄鱼数量 N(≤100)和007一次能跳跃的最大距离 D。随后 N 行,每行给出一条鳄鱼的 (x,y) 坐标。注意:不会有两条鳄鱼待在同一个点上。
输出格式:
如果007有可能逃脱,首先在第一行输出007需要跳跃的最少步数,然后从第二行起,每行给出从池心岛到岸边每一步要跳到的鳄鱼的坐标 (x,y)。如果没可能逃脱,就在第一行输出 0 作为跳跃步数。如果最短路径不唯一,则输出第一跳最近的那个解,题目保证这样的解是唯一的。

输入样例1:

17 15
10 -21
10 21
-40 10
30 -50
20 40
35 10
0 -10
-25 22
40 -40
-30 30
-10 22
0 11
25 21
25 10
10 10
10 35
-30 10

输出样例1

4
0 11
10 21
10 35
1

输入样例2:

4 13
-12 12
12 12
-12 -12
12 -12

输出样例2

0

数据结构:每个结点包含:鳄鱼坐标、到原点距离。

class Node {
public:
	int x, y;  //鳄鱼坐标
	double first_step; // 到远点距离
	bool operator<(const Node& t) const {
		return (this->first_step < t.first_step);
	}
};
Node node[N];

const int INF = 99999;
const int N = 101;
int dist[N]; 
int path[N]; // 到达i时,j是i的前缀,path[i] = j;
int n, d; //n 表示鳄鱼数,d为距离

INF设为最大值,没有到达的地方;N为鳄鱼的最大数量;
该题思路:利用BFS广度寻找能到达终点的路线。在BFS之前,需要将所有鳄鱼的位置遍历一次,将每个鳄鱼按离原点距离排序,将第一步能到达的鳄鱼push进队列中,用于初始BFS队列的元素。

for (int i = 0; i < n; i++) {
		int x, y;
		std::cin >> node[i].x >> node[i].y;
		node[i].first_step = std::sqrt(node[i].x * node[i].x + node[i].y * node[i].y);
	}
	std::sort(node, node + n);
	
for (int i = 0; i < n; i++) {
		if (node[i].first_step > 7.5 && node[i].first_step < d + 7.5) {
			q.push(i);
			dist[i] = 1;
		}
	}

接着是正常BFS思路,取首元素值再pop队列首元素,再搜索该元素附件的结点,符合push的就将其push进队列。但需要注意的是,拿出首元素后,需要判断下一步是否可以到达终点。对于四个象限,采用不同的方法计算靠近哪条边更近。如果都没法一步到达终点,那就继续循环。判断方式代码如下(temp_index为取出首元素的下标):

if (node[temp_index].x >= 0 && node[temp_index].y >= 0)
		distance_end = min_2(50 - node[temp_index].x, 50 - node[temp_index].y);
else if (node[temp_index].x >= 0 && node[temp_index].y <= 0)
		distance_end = min_2((50 - node[temp_index].x), (node[temp_index].y + 50));
else if (node[temp_index].x <= 0 && node[temp_index].y >= 0)
		distance_end = min_2(node[temp_index].x + 50, 50 - node[temp_index].y);
else if (node[temp_index].x <= 0 && node[temp_index].y <= 0)
		distance_end = min_2(node[temp_index].x + 50, node[temp_index].y + 50);
if (distance_end <= d) { // 如果下一步可以到终点
		if (dist[n] == INF) {
			dist[n] = dist[temp_index] + 1;
			path[n] = temp_index;  //更新最终结点的前缀
			break;
		}
}

无法下一步到达的话,则需要对所有没访问过的结点进行查找,只找出可以一步跳过去的结点,这也是选择BFS的原因,只搜索周围结点并插入队列中。在初始化中,会对dist数组全部赋予最大值,如果结点值等于最大值则视为没被访问过,再计算该点到首元素的距离,合适的话就插入队列中。值得注意的是,该题的最短路径是最短跳跃数,而非走过的距离,因此该题思路是可行的,只需要一步步慢慢靠近终点即可。对于符合的结点,在对应的dist数组上,加一操作,记为跳跃数,path数组记住前缀。

for (int i = 0; i < n; i++) {
			if (dist[i] == INF) {  //如果没被踩过
				double distance = std::sqrt((node[i].x - node[temp_index].x) * (node[i].x - node[temp_index].x) + (node[i].y - node[temp_index].y) * (node[i].y - node[temp_index].y));
				if (distance <= d) { // 距离够跳跃					
					q.push(i);
					dist[i] = dist[temp_index] + 1;
					path[i] = temp_index;
				}
			}
		}

那么我们再回过头到达终点的情况,因为这个方式是一次次循环的,基本上循环一次跳跃数才会加一,如果有先到达的结点,会将dist[n]赋值,赋的值一定是最快的循环次数所达到的路径,而至于是不是绕路,是不是最短distance,在该题是不关心的。同时,根据path[n]=temp_index,我们可以一次次回溯到初始结点,从而输出答案。

if (distance_end <= d) { // 如果下一步可以到终点
		if (dist[n] == INF) {
			dist[n] = dist[temp_index] + 1;
			path[n] = temp_index;  //更新最终结点的前缀
			break;
		}

输出结果时,只需要回溯,将结点push进栈中,再利用循环输出即可。

void Output( std::stack<int> & s) {
	if (!s.empty()) {
		int index = s.top();
		s.pop();
		std::cout << node[index].x << " " << node[index].y << std::endl;
		Output(s);
	}
}

int end_flag = path[n];

	while (end_flag != -1) {
		s.push(end_flag);
		end_flag = path[end_flag];
	}
	Output(s);

完整代码如下:


#include<iostream>
#include<queue>
#include<stack>
#include <algorithm>
#include<cmath>

const int INF = 99999;
const int N = 101;
int dist[N]; 
int path[N]; // 到达i时,j是i的前缀,path[i] = j;
int n, d; //n 表示鳄鱼数,d为距离

class Node {
public:
	int x, y;  //鳄鱼坐标
	double first_step; // 到远点距离
	bool operator<(const Node& t) const {
		return (this->first_step < t.first_step);
	}
};

Node node[N];

void Init_2() {
	for (int i = 0; i < N; i++) {
		dist[i] = INF;
		path[i] = -1;
	}
}

int min_2(int v1, int v2) {
	if (v1 >= v2) 
		return v2;
	else 
		return v1;
}

void BFS() {
	std::queue<int> q;  //采用BFS求最短路径
	double distance_end;
	//将第一步可以踩到的鳄鱼加入队列中
	for (int i = 0; i < n; i++) {
		if (node[i].first_step > 7.5 && node[i].first_step < d + 7.5) {
			q.push(i);
			dist[i] = 1;
			//path[i] = -1;
		}
	}

	while (!q.empty()) {
		int temp_index = q.front();
		q.pop();
		if (node[temp_index].x >= 0 && node[temp_index].y >= 0)
			distance_end = min_2(50 - node[temp_index].x, 50 - node[temp_index].y);
		else if (node[temp_index].x >= 0 && node[temp_index].y <= 0)
			distance_end = min_2((50 - node[temp_index].x), (node[temp_index].y + 50));
		else if (node[temp_index].x <= 0 && node[temp_index].y >= 0)
			distance_end = min_2(node[temp_index].x + 50, 50 - node[temp_index].y);
		else if (node[temp_index].x <= 0 && node[temp_index].y <= 0)
			distance_end = min_2(node[temp_index].x + 50, node[temp_index].y + 50);

		if (distance_end <= d) { // 如果下一步可以到终点
			if (dist[n] == INF) {
				dist[n] = dist[temp_index] + 1;
				path[n] = temp_index;  //更新最终结点的前缀
				break;
			}
		}

		for (int i = 0; i < n; i++) {
			if (dist[i] == INF) {  //如果没被踩过
				double distance = std::sqrt((node[i].x - node[temp_index].x) * (node[i].x - node[temp_index].x) + (node[i].y - node[temp_index].y) * (node[i].y - node[temp_index].y));
				if (distance <= d) { // 距离够跳跃					
					q.push(i);
					dist[i] = dist[temp_index] + 1;
					path[i] = temp_index;
				}
			}
		}
	}
}

void Output( std::stack<int> & s) {
	if (!s.empty()) {
		int index = s.top();
		s.pop();
		std::cout << node[index].x << " " << node[index].y << std::endl;
		Output(s);
	}
}


int main() {
	Init_2();
	std::cin >> n >> d;
	std::stack<int> s;

	//直径15,半径7.5,如果d>42.5则可以一步跳过
	if (d >= 42.5) {
		std::cout << 1 << std::endl;
		return 0;
	}

	for (int i = 0; i < n; i++) {
		int x, y;
		std::cin >> node[i].x >> node[i].y;
		node[i].first_step = std::sqrt(node[i].x * node[i].x + node[i].y * node[i].y);
	}
	std::sort(node, node + n);
	BFS();
	if (dist[n] == INF) {//没成功上岸
		std::cout << "0" << std::endl;
		return 0;
	}
	//输出步伐
	std::cout << dist[n] << std::endl;
	int end_flag = path[n];
	while (end_flag != -1) {
		s.push(end_flag);
		end_flag = path[end_flag];
	}
	Output(s);
}

1-3最小生成树的唯一性

题目

给定一个带权无向图,如果是连通图,则至少存在一棵最小生成树,有时最小生成树并不唯一。本题就要求你计算最小生成树的总权重,并且判断其是否唯一。

输入格式:

首先第一行给出两个整数:无向图中顶点数 N(≤500)和边数 M。随后 M 行,每行给出一条边的两个端点和权重,格式为“顶点1 顶点2 权重”,其中顶点从 1 到N 编号,权重为正整数。题目保证最小生成树的总权重不会超过 2^30。

输出格式:

如果存在最小生成树,首先在第一行输出其总权重,第二行输出“Yes”,如果此树唯一,否则输出“No”。如果树不存在,则首先在第一行输出“No MST”,第二行输出图的连通集个数。

输入样例1:

5 7
1 2 6
5 1 1
2 3 4
3 4 3
4 1 7
2 4 2
4 5 5

输入样例1:

11
Yes

输入样例2:

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

输出样例2:

4
No

输入样例3:

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

输出样例3:

No MST
2

该题数据结构如下:

struct Edge
{
	int u, v, w;
};
int N, M, f[501];
int Graph[501][501];
Edge edge[250000];
bool vis[501];

Edge中,u表示出发结点,v表示达到结点,w表示此边的权重,同时在输入数据初始化时,会将Graph能到达的边赋予w权重值。

其中,该算法两个核心的函数步骤: find()函数可以找到该结点的根节点,而merge()函数是将添加的新边,连接起来,方便find()函数找到其根节点,具体步骤会在下文解释。

int find(int x) {
	if (x == f[x]) {
		return x;
	}
	return f[x] = find(f[x]);
}

void merge(int a, int b) {
	int u = find(a);
	int v = find(b);
	f[v] = u;
}

需要找到最小生成树,那么需要利用克鲁斯卡尔算法(kruskal),将所有边的权重进行排序,逐步添加权重最小的边,同时边不能形成环。

std::sort(edge, edge + M, cmp);

根据边数进行循环,当插入的边达到N-1时,循环结束。而对于每条边,都需要看看这条边能否插入,是否会形成环。如何判断是否会形成环呢,我们先看看一开始的两三条边是如何插入的。

sum += edge[i].w;
merge(u, v);
cnt++;
if (cnt == N - 1) {
	break;

void merge(int a, int b) {
	int u = find(a);
	int v = find(b);
	f[v] = u;
}

加入一条边后,sum表示总权重值,需要加对应的值。同时执行merge函数,这时我们再来看看merge函数是如何执行的。假如添加的边为 1 2 1 那么u为1,v为2,此时会将f[2] = 1。因此 f[] 数组中2的元素存的是根节点元素。那么再插入 2 3 1 呢?u = find(2) = 1,因此f[3] = 1。
我们可以知道添加的所有结点都是指向根节点的,当再添加 3 1 1 时,会发现find(3) == find(1) ,因此会形成环,无法添加。

那么,最小生成树可以正常生成了,如何判断是否为唯一呢?按我的理解举例说明:

1 2 1
2 3 1

假设我们添加了这两条边,1 2 3已经连成一条线,此时,接着的两条边都可以添加,同时都连完了所有结点,权重也是一样的。

3 4 2
4 1 2

因此,如果不是唯一的,必有两条权重可以相互替换,可任意使用其中一条,那么代码思路如下:

for (int j = i + 1; j < M && edge[i].w == edge[j].w; j++) {
    			int u1 = find(edge[j].u);
    			int v1 = find(edge[j].v);
    			if (u1 == u && v1 == v || u1 == v && v1 == u) {
    				flag = false;
    				break;
    			}

当我们正常添加了3 4 2 以后,1 2 3 4 的 f[]都指向根节点 1 ,那么我们继续查看若干条权重一样的边,如果后续的边是一个用途,那么直接判断不唯一。例如:3 4 2 的边,注意我们此时还没执行merge操作,3 4 经过find可以视为 4连入了1结点形成的子树。而我们查看后续的边,发现 4 1 2 权重一样,也是将4 连入1结点形成的子树,只是顺序不同,因此u1 == u 和 u1 == v都考虑了。这样我们便可以查到该子树是否唯一。

完整代码如下:

#include<iostream>
#include <algorithm>

struct Edge
{
	int u, v, w;
};
int N, M, f[501];
int Graph[501][501];
Edge edge[250000];
bool vis[501];

void dfs(int x) {
	vis[x] = true;
	for (int i = 1; i <= N; i++) {
		if (Graph[x][i] && !vis[i]) {
			dfs(i);
		}
	}
}

int Count() {
	int cnt = 0;
	for (int i = 1; i <= N; i++) {
		if (!vis[i]) {
			cnt++;
			dfs(i);
		}
	}
	return cnt;
}



void Init() {
	for (int i = 0; i <= N; i++) {
		f[i] = i;
	}

	for (int i = 0; i < M; i++) {
		int u, v, w;
		std::cin >> u >> v >> w;
		edge[i].u = u;
		edge[i].v = v;
		edge[i].w = w;
		Graph[u][v] = w;
		Graph[v][u] = w;

	}

}


int find(int x) {
	if (x == f[x]) {
		return x;
	}
	return f[x] = find(f[x]);
}

void merge(int a, int b) {
	int u = find(a);
	int v = find(b);
	f[v] = u;
}

bool cmp(Edge e1, Edge e2) {
	return e1.w < e2.w;
}

int main() {
    std::cin >> N >> M;
    Init();
    std::sort(edge, edge + M, cmp);
    bool flag = true;
    int sum = 0; int cnt = 0;
    for (int i = 0; i < M; i++) {
    	int u = find(edge[i].u);
    	int v = find(edge[i].v);
    	//排除添加一条边变成环
    	if (u != v) {
    		for (int j = i + 1; j < M && edge[i].w == edge[j].w; j++) {
    			int u1 = find(edge[j].u);
    			int v1 = find(edge[j].v);
    			if (u1 == u && v1 == v || u1 == v && v1 == u) {
    				flag = false;
    				break;
    			}
    		}
    		sum += edge[i].w;
    		merge(u, v);
    		cnt++;
    		if (cnt == N - 1) {
    			break;
    		}
    	}
    }
    int num = Count();
    if (num == 1) {
    	std::cout << sum << std::endl;
    	if (flag) {
    		std::cout << "Yes";
    	}
    	else {
    		std::cout << "No";
    	}
    }
    else {
    	std::cout << "No MST" << std::endl;
    	std::cout << num;
    }
}
	



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值