1.并查集
题目:
某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府―畅通工程的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?输入包含若干测试用例。每个测试用例的第 1 行给出两个正整数,分别是城镇数目 N ( < 1000 )和道路数目 M;随后的 M 行对应 M 条道路,每行给出 一对正整数,分别是该条道路直接连通的两个城镇的编号。为简单起见,城镇从 1 到 N 编号。当 N 为 0 时,输入结束,该用例不被处理。
输入:
4 2
1 3
4 3
3 3
1 2
1 3
2 3
5 2
1 2
3 5
999 0
0
输出:
1
0
2
998
来源:
2005年浙江大学计算机及软件工程研究生机试真题
思路:
该问题可以被抽象成在一个图上查找连 通分量(彼此连通的结点集合)的个数,我们只需求得连通分量的个数,就能得到答案(新建一些边将这些连通分量连通)。这个问题可以使用并查集完成,初始时,每个结点都是孤立的连通分量,当读入已经建成的边后,我们将边的两个顶点所在集合合并,表示这两个集合中的所有结点已经连通。对所有的边重复该操作,最后计算所有的结点被保存在几个集合中,即存在多少棵树就能得知共有多少个连通分量(集合)。
代码:
#include <bits/stdc++.h>
using namespace std;
int Tree[1000];//并查集
int find_root(int x)
{
if(Tree[x] == -1)
return x;
else
{//压缩路径
int tmp = find_root(Tree[x]);
Tree[x] = tmp;
return tmp;
}
}
int main()
{
int n, m;
while(scanf("%d", &n) != EOF && n != 0)
{
scanf("%d", &m);
for(int i = 1; i <= n; i++)
Tree[i] = -1;//每个节点初始化为一个集合
while(m--)
{
int a, b;
scanf("%d%d", &a, &b);
a = find_root(a);
b = find_root(b);
if(a != b) Tree[a] = b;//若a,b不在一个连通分量(集合)中 则合并
}
int ans = 0;
for(int i = 1; i <= n; i++)
if(Tree[i] == -1) ans++;//统计连通分量(集合)的个数
printf("%d\n", ans-1);//n个连通分量只需n-1条边
}
return 0;
}
题目:
Mr Wang wants some boys to help him with a project. Because the project is rather complex, the more boys come, the better it will be. Of course there are certain requirements.Mr Wang selected a room big enough to hold the boys. The boy who are not been chosen has to leave the room immediately. There are 10000000 boys in the room numbered from 1 to 10000000 at the very beginning. After Mr Wang’s selection any two of them who are still in this room should be friends (direct or indirect), or there is only one boy left. Given all the direct friend-pairs, you should decide the best way.
Input: The first line of the input contains an integer n (0 ≤ n ≤ 100 000) - the number of direct friend-pairs. The following n lines each contains a pair of numbers A and B separated by a single space that suggests A and B are direct friends. (A ≠ B, 1 ≤ A, B ≤ 10000000)
Output: The output in one line contains exactly one integer equals to the maximum number of boys Mr Wang may keep.
大意:
有 10000000 个小朋友,他们之中有 N 对好朋友,且朋友关系具 有传递性:若 A 与 B 是朋友,B 与 C 是朋友,那么我们也认为 A 与 C 是朋友。 在给出这 N 对朋友关系后,要求我们找出一个最大(人数最多)的集合,该集 合中任意两人之间都是朋友或者该集合中只有一个人,输出该最大人数。
输入:
4
1 2
3 4
5 6
1 6
4
1 2
3 4
5 6
7 8
输出:
4
2
来源:
九度OJ 1444
思路:
如前例所示,我们利用并查集相关操作已经可以求得有几个这样符合条件的 集合,但是计算集合中的元素个数我们仍没有涉及。我们如果能够成功求得每 集合的元素个数,我们只需要选择包含元素最多的集合,并输出该集合中的元素个数即可。所以,我们将之前每个根结点的值做一点改动,让其值的绝对值为该集合中节点个数,例如某根节点的值为-23,表示该集合中共有23个节点。
代码:
#include <bits/stdc++.h>
using namespace std;
#define MAXN 10000001
int Tree[MAXN];
int find_root(int x)
{
if(Tree[x] < 0)//判断条件不再是''==-1''
return x;
else
{//压缩路径
int tmp = find_root(Tree[x]);
Tree[x] = tmp;
return tmp;
}
}
int main()
{
int n;
while(scanf("%d", &n) != EOF)
{
for(int i = 1; i < MAXN; i++)
Tree[i] = -1;//初始化每个集合只有一个节点
while(n--)
{
int a, b;
scanf("%d%d", &a, &b);
a = find_root(a);
b = find_root(b);
if(a != b)
{
Tree[b] += Tree[a];//计算合并后的节点和
Tree[a] = b;
}
}
int ans = -1;//答案至少为1
for(int i = 0; i < MAXN; i++)
if(Tree[i] < ans) ans = Tree[i];//统计集合中最多节点数
printf("%d\n", abs(ans));//输出其绝对值
}
return 0;
}
总结:
并查集可以求图论中的连通分量相关问题,要合理运用。
2.最小生成树(MST)
题目:
某省调查乡村交通状况,得到的统计表中列出了任意两村庄间的距离。省政 府―畅通工程的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有 直接的公路相连,只要间接通过公路可达即可),并要求铺设的公路总长度为最小。请计算最小的公路总长度。测试输入包含若干测试用例。每个测试用例的第 1 行给出村庄数目 N ( < 100 );随后的 N(N-1)/2 行对应村庄间的距离,每行给出一对正整数,分别是两 个村庄的编号,以及此两村庄间的距离。为简单起见,村庄从 1 到 N 编号。当 N 为 0 时,输入结束,该用例不被处理。对每个测试用例,在 1 行里输出最小的公路总长度。若不存在则输出-1。
输入:
3
1 2 1
1 3 2
2 3 4
4
1 2 1
1 3 4
1 4 1
2 3 3
2 4 2
3 4 5
3
1 2 1
1 2 2
1 2 3
0
输出:
3
5
-1
来源:
思路:
在给定的道路中选取一些,使所有的城市直接或间接连通且使道路的总长度 最小,该例即为典型的最小生成树问题。我们将城市抽象成图上的结点,将道路 抽象成连接点的边,其长度即为边的权值。经过这样的抽象,我们求得该图的最小生成树,其上所有的边权和即为所求。
代码:
#include <bits/stdc++.h>
using namespace std;
#define N 101
int Tree[N];
int find_root(int x)
{
if(Tree[x] == -1)
return x;
else
{//压缩路径
int tmp = find_root(Tree[x]);
Tree[x] = tmp;
return tmp;
}
}
struct Edge
{
int a;
int b;
int cost;
bool operator < (const Edge &b) const//重载小于号使其可按边权重升序排列
{
return cost < b.cost;
}
}edge[N*N/2];
int main()
{
int n;
while(scanf("%d", &n) != EOF && n != 0)
{
for(int i = 1; i <= n*(n-1)/2; i++)
scanf("%d%d%d", &edge[i].a, &edge[i].b, &edge[i].cost);
sort(edge+1, edge+1+n*(n-1)/2);//按边权重升序排列
fill(Tree+1, Tree+1+n, -1);//初始化集合
int ans = 0;
for(int i = 1; i <= n*(n-1)/2; i++)
{
int a = find_root(edge[i].a);
int b = find_root(edge[i].b);
if(a != b)//a和b属于不同连通分量(集合)
{
Tree[a] = b;//合并
ans += edge[i].cost;//累加边权重
}
}
if(count(Tree+1, Tree+1+n, -1) == 1)//只有一个集合说明该图有最小生成树
printf("%d\n", ans);
else
printf("-1\n");//非连通图输出-1
}
return 0;
}
题目:
In an episode of the Dick Van Dyke show, little Richie connects the freckles on his Dad’s back to form a picture of the Liberty Bell. Alas, one of the freckles turns out to be a scar, so his Ripley’s engagement falls through.Consider Dick’s back to be a plane with freckles at various (x,y) locations. Your job is to tell Richie how to connect the dots so as to minimize the amount of ink used. Richie connects the dots by drawing straight lines between pairs, possibly lifting the pen between lines. When Richie is done there must be a sequence of connected lines from any freckle to any other freckle.
Input: The first line contains 0 < n <= 100, the number of freckles on Dick’s back. For each freckle, a line follows; each following line contains two real numbers indicating the (x,y) coordinates of the freckle.
Output: Your program prints a single real number to two decimal places: the minimum total length of ink lines that can connect all the freckles.
输入:
3
1.0 1.0
2.0 2.0
2.0 4.0
输出:
3.41
来源:
2009 年北京大学计算机研究生机试真题
大意:
平面上有若干个点,我们需要用一些线段来将这些点连接起来使 任意两个点能够通过一系列的线段相连,给出所有点的坐标,求一种连接方式使 所有线段的长度和最小,求该长度和。
思路:
若我们将平面上的点抽象成图上的结点,将结点间直接相邻的线段抽象成连
接结点的边,且权值为其长度,那么该类似于几何最优值的问题就被我们转化到 了图论上的最小生成树问题。但在开始求最小生成树前,我们必须先建立该图, 得出所有的边和相应的权值。
代码:
#include <bits/stdc++.h>
using namespace std;
#define N 101
int Tree[N];
int find_root(int x)
{
if(Tree[x] == -1)
return x;
else
{
int tmp = find_root(Tree[x]);
Tree[x] = tmp;
return tmp;
}
}
struct Edge
{
int a;
int b;
double cost;
bool operator < (const Edge &b) const
{
return cost < b.cost;
}
}edge[N*N/2];
struct Point
{
double x;
double y;
double Get_distance(const Point &b) const//计算两点距离
{
return sqrt(pow(x-b.x, 2)+pow(y-b.y, 2));
}
}point[N];
int main()
{
int n;
while(scanf("%d", &n) != EOF)
{
for(int i = 1; i <= n; i++)
scanf("%lf%lf", &point[i].x, &point[i].y);
int num = 1;
for(int i = 1; i <= n; i++)
for(int j = i+1; j <= n; j++)
{
edge[num].a = i;
edge[num].b = j;
edge[num++].cost = point[i].Get_distance(point[j]);
}
sort(edge+1, edge+1+n);//按边权重升序排列
fill(Tree+1, Tree+1+n, -1);//初始化集合
double ans = 0;
for(int i = 1; i <= n; i++)
{
int a = find_root(edge[i].a);
int b = find_root(edge[i].b);
if(a != b)//a和b属于不同连通分量(集合)
{
Tree[a] = b;//合并
ans += edge[i].cost;//累加边权重
}
}
printf("%.2f\n", ans);
}
return 0;
}
总结:
以上的最小生成树都是使用Kruskal 算法来解答的,还可以采用Prime算法来解决最小生成树问题,这里不再赘述。
3.最短路径
题目:
在每年的校赛里,所有进入决赛的同学都会获得一件很漂亮的 t-shirt。但是 每当我们的工作人员把上百件的衣服从商店运回到赛场的时候,却是非常累的 ! 所以现在他们想要寻找最短的从商店到赛场的路线,你可以帮助他们吗? 输入包括多组数据。每组数据第一行是两个整数 N、M(N<=100,M<=10000), N 表示成都的大街上有几个路口,标号为 1 的路口是商店所在地,标号为 N 的路口是赛场所在地,M 则表示在成都有几条路。N=M=0 表示输入结束。接下来 M 行,每行包括 3 个整数 A,B,C(1<=A,B<=N,1<=C<=1000),表示在路口 A 与路口 B 之间有一条路,我们的工作人员需要 C 分钟的时间走过这条路。输入 保证至少存在 1 条商店到赛场的路线。 当输入为两个 0 时,输入结束。 对于每组输入,输出一行,表示工作人员从商店走到赛场的最短时间。
输入:
2 1
1 2 3
3 3
1 2 5
2 3 5
3 1 2
0 0
输出:
3
2
来源:
九度OJ1447
思路:
求最短路径我们可以使用Floyd算法来计算出任意两点之间的最短路径,然后取出我们所需起点到终点的路径。
代码:
#include <bits/stdc++.h>
using namespace std;
#define N 101
int cost[N][N];
void init(int n)
{
for(int i = 1; i <=n; i++)
for(int j = 1; j <= n; j++)
{
if(i != j)
cost[i][j] = -1;//-1代表无穷大
else
cost[i][j] = 0;
}
}
int main()
{
int n, m;
while(scanf("%d%d", &n, &m) != EOF && n !=0 && m != 0)
{
init(n);
int a, b, c;
for(int i = 0; i < m; i++)
{
scanf("%d%d%d", &a, &b, &c);
cost[a][b] = cost[b][a] = c;//无向图a到b和b到a等价
}
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
{
if(cost[i][k] == -1 || cost[k][j] == -1) continue;
if(cost[i][j] == -1 || cost[i][j] > cost[i][k]+cost[k][j])
cost[i][j] = cost[i][k]+cost[k][j];
}
printf("%d\n", cost[1][n]);
}
return 0;
}
解法二:
#include <bits/stdc++.h>
using namespace std;
#define N 1001
#define INF 0x3f3f3f3f//表示无穷大
struct node
{
int v;
int cost;
node(){}
node(int vv, int cc):v(vv), cost(cc){}
bool operator < (const node& b) const
{
return cost > b.cost;
}
};
int dis[N];
int edge[N][N];
void Dijkstra(int n)
{
priority_queue<node> Q;
for(int i = 1; i <= n; i++)
dis[i] = INF;//初始化为无穷大
Q.push(node(1, 0));//源点入队
dis[1] = 0;//源点到源点的距离置为0
while(!Q.empty())
{
node t = Q.top();//取堆顶元素
Q.pop();
int now = t.v;
for(int i = 1; i <= n; i++)
{
if(edge[now][i] != INF && dis[i] > dis[now] + edge[now][i])
{
dis[i] = dis[now] + edge[now][i];//更新路径
Q.push(node(i, dis[i]));//入队
}
}
}
}
int main()
{
int n, m;
while(scanf("%d%d", &n, &m) != EOF)
{
if(n == 0 && m == 0) break;
memset(edge, INF, sizeof(edge));//初始化为各点互相不可达
for(int i = 1; i <= n; i++) edge[i][i] = 0;//到自己的距离为0
int a, b, c;
for(int i = 0; i < m; i++)
{
scanf("%d%d%d", &a, &b, &c);
edge[a][b] = edge[b][a] = c;
}
Dijkstra(n);
printf("%d\n", dis[n]);
}
return 0;
}
总结:
解法二使用了Dijkstra算法来求最短路径,关于最短路径的更多解法,以及处理带负权重的环的处理见对求最短路径常见算法的简单总结。
4.拓扑排序
引入:
我们先讨论如何求一个有向无环图的拓 扑序列,即拓扑排序的方法。 首先,所有有入度(即以该结点为弧头的弧的个数)的结点均不可能排在第 一个。那么,我们选择一个入度为 0 的结点,作为序列的第一个结点。当该结点 被选为序列的第一个顶点后,我们将该点从图中删去,同时删去以该结点为弧尾 的所有边,得到一个新图。那么这个新图的拓扑序列即为原图的拓扑序列中除去 第一个结点后剩余的序列。同样的,我们在新图上选择一个入度为 0 的结点,将 其作为原图的第二个结点,并在新图中删去该点以及以该点为弧尾的边。这样我 们又得到了一张新图,重复同样的方法,直到所有的结点和边都从原图中删去。 若在所有结点尚未被删去时即出现了找不到入度为 0 的结点的情况,则说明剩余 的结点形成一个环路,拓扑排序失败,原图不存在拓扑序列。
题目:
ACM-DIY is a large QQ group where many excellent acmers get together. It is so harmonious that just like a big family. Every day,many “holy cows” like HH, hh, AC, ZT, lcc, BF, Qinz and so on chat on-line to exchange their ideas. When someone has questions, many warm-hearted cows like Lost will come to help. Then the one being helped will call Lost “master”, and Lost will have a nice “prentice”. By and by, there are many pairs of “master and prentice”. But then problem occurs: there are too many masters and too many prentices, how can we know whether it is legal or not?We all know a master can have many prentices and a prentice may have a lot of masters too, it’s legal. Nevertheless , some cows are not so honest, they hold illegal relationship. Take HH and 3xian for instant, HH is 3xian’s master and, at the same time, 3xian is HH’s master,which is quite illegal! To avoid this,please help us to judge whether their relationship is legal or not. Please note that the “master and prentice” relation is transitive. It means that if A is B’s master ans B is C’s master, then A is C’s master.
Iuput: The input consists of several test cases. For each case, the first line contains two integers, N (members to be tested) and M (relationships to be tested)(2 <= N, M <= 100). Then M lines follow, each contains a pair of (x, y) which means x is y’s master and y is x’s prentice. The input is terminated by N = 0.TO MAKE IT SIMPLE, we give every one a number (0, 1, 2,…, N-1). We use their numbers instead of their names.
Output: For each test case, print in one line the judgement of the messy relationship.If it is legal, output “YES”, otherwise “NO”.
输入:
3 2
0 1
1 2
2 2
0 1
1 0
0 0
输出:
YES
NO
来源:
九度OJ1448
大意:
在一个 qq 群里有着许多师徒关系,如 A 是 B 的师父,同时 B 是 A 的徒弟,一个师父可能有许多徒弟,一个徒弟也可能会有许多不同的师父。 输入给出该群里所有的师徒关系,问是否存在这样一种非法的情况:以三个人为 例,即 A 是 B 的师父,B 是 C 的师父,C 又反过来是 A 的师父。若我们将该群 里的所有人都抽象成图上的结点,将所有的师徒关系都抽象成有向边(由师父指 向徒弟),该实际问题就转化为一个数学问题——该图上是否存在一个环,即判 断该图是否为有向无环图。
思路:
无论何时,当需要判断某个图是否属于有向无环图时,我们都需要立刻联想 到拓扑排序。若一个图,存在符合拓扑次序的结点序列,则该图为有向无环图; 反之,该图为非有向无环图。也就是说,若在该图上拓扑排序成功,该图为有向无环图;反之,则存在环路。
代码:
#include <bits/stdc++.h>
using namespace std;
#define N 101
int indegree[N];//保存每个节点入度
vector<int> edge[N];//模拟邻接表
stack<int> S;//保存入度为0的节点
int main()
{
int n, m;
while(scanf("%d%d", &n, &m) != EOF)
{
if(n == 0 && m == 0) break;
//初始化操作
fill(indegree, indegree+n, 0);
for(int i = 0; i < n; i++) edge[i].clear();
while(!S.empty()) S.pop();
int u, v;
for(int i = 0; i < m; i++)
{
scanf("%d%d", &u, &v);//u--->v
edge[u].push_back(v);//u链接到v
indegree[v]++;//v的入度+1
}
for(int i = 0; i < n; i++)//入度为0的节点入栈
if(indegree[i] == 0)
S.push(i);
int cnt = 0;//保存已输出节点个数
while(!S.empty())
{
int now = S.top();
S.pop();
cnt++;
for(int i = 0; i < edge[now].size(); i++)
if(--indegree[edge[now][i]] == 0)//入度-1 若变为0则入栈
S.push(edge[now][i]);
}
if(cnt == n)
printf("YES\n");
else
printf("NO\n");
}
return 0;
}