贪心算法
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;
}
}