并查集
int findRoot(int x) {
if (Tree[x] == -1)
return x;
else
return findRoot(Tree[x]);
}
int findRoot(int x) {
int ret;
while (Tree[x] != -1)
x = Tree[x];
ret = x;
return ret;
}
int findRoot(int x) {
if (Tree[x] == -1)
return x;
else {
int tmp = findRoot(Tree[x]);
Tree[x] = tmp; // 将当前节点的双亲节点设置为查找返回的根节点编号
return tmp;
}
}
int findRoot(int x) {
int ret;
int tmp = x;
while (Tree[x] != -1)
x = Tree[x];
ret = x;
x = tmp; // 在做一次从节点x到根节点的遍历
while (Tree[x] != -1) {
int t = Tree[x];
Tree[x] = ret;
x = t; // 遍历过程中将这些节点的双亲节点都设置为已经查找到的根节点编号
}
return ret;
}
#include <iostream>
using namespace std;
const int N = 1000;
int Tree[N];
int findRoot(int x) { // 查找某个节点所在的树的根节点
if (Tree[x] == -1)
return x;
else {
int tmp = findRoot(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 = findRoot(a);
b = findRoot(b); // 查找边的两个顶点所在集合信息
if (a != b) // 若两个顶点不在同一个集合则合并这两个集合
Tree[a] = b;
}
int ans = 0;
for (int i = 1; i <= n; i++) {
if (Tree[i] == -1)
ans++; // 统计所有节点中根节点的个数
}
printf("%d\n", ans-1); // 答案即为在ans个集合间再修建ans-1条道路即可使所有节点连通
}
return 0;
}
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10000001;
int Tree[N];
int sum[N];
int findRoot(int x) { // 查找某个节点所在的树的根节点
if (Tree[x] == -1)
return x;
else {
int tmp = findRoot(Tree[x]);
Tree[x] = tmp;
return tmp;
}
}
int main() {
int n;
while (scanf("%d", &n) != EOF) {
for (int i = 1; i < N; i++) {
Tree[i] = -1;
sum[i] = 1;
}
while (n--) {
int a, b;
scanf("%d%d", &a, &b);
a = findRoot(a);
b = findRoot(b);
if (a != b)
Tree[a] = b;
sum[b] += sum[a];
}
int ans = 1;
for (int i = 1; i < N; i++) {
if (sum[i] > ans)
ans = sum[i];
}
printf("%d\n", ans);
}
return 0;
}
最小生成树(MST)
Kruskal算法
- 初始时多有节点属于孤立的集合
- 按照边权递增顺序遍历所有的边,若遍历到的边两个顶点仍分属不同的集合(该边即为连通这两个集合的边中权值最小的那条)则确定该边为最小生成树上的一条边,并将这两个顶点分属的集合合并
- 遍历完所有边后,原图上所有节点属于同一个集合则被选取的边和原图中所有节点构成最小生成树,否则原图不连通,最小生成树不存在
如上步骤所示,在用Kruskal算法求解最小生成树的过程中涉及大量的集合操作,恰好可以使用并查集来实现这些操作。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 101;
int Tree[N];
int findRoot(int x) { // 查找某个节点所在的树的根节点
if (Tree[x] == -1)
return x;
else {
int tmp = findRoot(Tree[x]);
Tree[x] = tmp;
return tmp;
}
}
struct Edge {
int a, b; // 边两个顶点的编号
int cost; // 该边的权值
bool operator < (const Edge& A) const { // 重载小于号使其可以按照边权从小到大排列
return cost < A.cost;
}
} edge[6000];
int main() {
int n;
while (scanf("%d", &n) != EOF) {
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); // 按照边权值递增排列所有边
for (int i = 1; i <= n; i++) {
Tree[i] = -1;
}
int ans = 0;
for (int i = 1; i <= n*(n-1)/2; i++) { // 按照边权值递增顺序遍历所有边
int a = findRoot(edge[i].a);
int b = findRoot(edge[i].b);
if (a != b) {
Tree[a] = b;
ans += edge[i].cost;
}
}
printf("%d\n", ans);
}
return 0;
}
#include <iostream>
#include <cmath>
#include <algorithm>
using namespace std;
const int N = 101;
int Tree[N];
int findRoot(int x) { // 查找某个节点所在的树的根节点
if (Tree[x] == -1)
return x;
else {
int tmp = findRoot(Tree[x]);
Tree[x] = tmp;
return tmp;
}
}
struct Edge {
int a, b; // 边两个顶点的编号
double cost; // 该边的权值
bool operator < (const Edge& A) const { // 重载小于号使其可以按照边权从小到大排列
return cost < A.cost;
}
} edge[6000];
struct Point {
double x, y;
double getDistance(const Point& p) {
double tmp = (x-p.x)*(x-p.x) + (y-p.y)*(y-p.y);
return sqrt(tmp);
}
} points[101];
int main() {
int n;
while (scanf("%d", &n) != EOF) {
for (int i = 1; i <= n*(n-1)/2; i++) {
scanf("%lf%lf", &points[i].x, &points[i].y);
}
int size = 0; // 抽象出边的总数
for (int i = 1; i <= n; i++) {
for (int j = i+1; j <= n; j++) { // 连接两点的线段抽象成边
edge[size].a = i;
edge[size].b = j; // 该边的两个顶点编号
edge[size].cost = points[i].getDistance(points[j]); // 边权值为两点之间的长度
size++;
}
} // 遍历所有的点对
sort(edge, edge+size); // 按照边权值递增排列所有边
for (int i = 1; i <= n; i++) {
Tree[i] = -1;
}
double ans = 0;
for (int i = 0; i < size; i++) {
int a = findRoot(edge[i].a);
int b = findRoot(edge[i].b);
if (a != b) {
Tree[a] = b;
ans += edge[i].cost;
}
}
printf("%.2lf\n", ans);
}
return 0;
}
最短路径
Floyd算法
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (ans[i][k] == INFINITY || ans[k][j] == INFINITY)
continue;
// 对于第一种情况,ans[i][k]和ans[k][j]都不是INFINITY,k与i和j都连通,则ans[i][j]不会再是INFINITY
if (ans[i][j] == INFINITY || ans[i][k] + ans[k][j] < ans[i][j])
ans[i][j] = ans[i][k] + ans[k][j];
}
}
}
#include<iostream>
using namespace std;
int ans[101][101];
int main() {
int n, m;
while (scanf("%d%d", &n, &m) != EOF) {
if (n == 0 && m == 0)
break;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
ans[i][j] = -1; // 对邻接矩阵初始化,我们用-1代表无穷
}
ans[i][i] = 0; // 自己到自己的路径长度设为0
}
while (m--) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
ans[a][b] = ans[b][a] = c;
}
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (ans[i][k] == -1 || ans[k][j] == -1)
continue;
if (ans[i][j] == -1 || ans[i][k] + ans[k][j] < ans[i][j])
ans[i][j] = ans[i][k] + ans[k][j];
}
}
}
printf("%d\n", ans[1][n]);
}
return 0;
}
Dijkstra算法
算法流程:
- 初始化,集合K中加入节点1,节点1到节点1的最短距离为0,到其他节点为无穷(或不确定)。
- 遍历与集合K中节点直接相邻的边
(U, V, C)
,其中U属于结合K,V不属于集合K,计算由节点1出发按照已经得到的最短路径到达U,再由U经过该边到达V时的路径长度。比较所有与集合K中节点直接相邻的非集合K节点该路径长度,其中路径长度最小的节点被确定为下一个最短路径确定的节点,其最短路径长度即为这个路径长度,最后将该节点加入集合K。 - 若集合K中已经包含了所有的点,算法结束,否则重复步骤2
#include <vector>
#include <iostream>
using namespace std;
struct E { // 邻接链表中链表元素结构体
int next; // 代表直接相邻的节点
int c; // 代表该边的权值(长度)
};
vector<E> edge[101]; // 邻接链表
bool mark[101]; // 标记,当mark[j]为true时表示节点j的最短路径长度已经得到,该节点已经加入集合K
/*距离向量,当mark[i]为true时,表示已得到的最短路径长度;
否则,表示所有从节点1出发,经过已知的最短路径达到集合K中的某节点,再经过一条边到达节点i的路径中最短的距离
*/
int Dis[101];
int main() {
int n, m;
while (scanf("%d%d", &n, &m) != EOF){
if (n == 0 && m== 0)
break;
for (int i = 1; i <= n; i++)
edge[i].clear(); // 初始化邻接链表
while (m--) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
E tmp;
tmp.c = c;
tmp.next = b;
edge[a].push_back(tmp);
tmp.next = a;
edge[b].push_back(tmp);
}
for (int i = 1; i <= n; i++) { // 初始化
Dis[i] = -1; // 所有距离为-1,即不可达
mark[i] = false; // 所有节点不属于集合K
}
Dis[1] = 0; // 得到最近的点为节点1,长度为0
mark[1] = true; // 将节点1加入集合K
int newP = 1; // 集合K中新加入的点为节点1
for (int i = 1; i < n; i++) { // 循环n-1次,按照最短路径递增的顺序确定
for (int j = 0; j < edge[newP].size(); j++) {
int t = edge[newP][j].next; // 该边的另一个节点
int c = edge[newP][j].c; // 该边的长度
if (mark[t] == true) // 若另一节点也属于K,则跳过
continue;
// 若该店尚不可达,或者该节点从新加入的节点经过一条边到达时比以往距离更短
if (Dis[t] == -1 || Dis[t] > Dis[newP] + c)
Dis[t] = Dis[newP] + c;
}
int min = 123123123; // 最小值初始化为一个大整数,为找最小值做准备
for (int j = 1; j <= n; j++) {
if (mark[j] == true)
continue;
if (Dis[j] != -1 && Dis[j] < min) {
min = Dis[j];
newP = j;
}
}
mark[newP] = true;
}
// printf("%d %d\n", Dis[T], cost[T]); // 输出答案
}
return 0;
}
#include <iostream>
#include <vector>
using namespace std;
struct E {
int next;
int d;
int p;
E(int _n, int _d, int _p) : next(_n), d(_d), p(_p) {}
} ;
vector<E> edge[1005];
bool visited[1005];
int Dis[1005];
int Spend[1005];
int main() {
int n, m;
while (scanf("%d%d", &n, &m) != EOF) {
if (n == 0 && m == 0)
break;
for (int i = 1; i<= n; i++)
edge[i].clear();
int a, b, d, p;
while (m--) {
scanf("%d%d%d%d", &a, &b, &d, &p);
E tmp(b, d, p);
edge[a].push_back(tmp);
tmp.next = a;
edge[b].push_back(tmp);
}
for (int i = 1; i <= n; i++) {
visited[i] = false;
Dis[i] = -1;
Spend[i] = 9999999;
}
int s, t;
scanf("%d%d", &s, &t);
int newP = s;
visited[newP] = true;
Dis[newP] = 0;
Spend[newP] = 0;
for (int i = 1; i < n; i++) {
for (int j = 0; j < edge[newP].size(); j++) {
int next = edge[newP][j].next;
int d = edge[newP][j].d;
int p = edge[newP][j].p;
if (visited[next])
continue;
if (Dis[next] == -1 || Dis[next] > Dis[newP] + d ||
(Dis[next] == Dis[newP] + d && Spend[next] > Spend[newP] + p)) {
Dis[next] = Dis[newP] + d;
Spend[next] = Spend[newP] + p;
}
}
int min = 9999999;
for (int i = 1; i <= n; i++) {
if (visited[i])
continue;
if (Dis[i] != -1 && Dis[i] < min) {
min = Dis[i];
newP = i;
}
}
visited[newP] = true;
}
printf("%d %d\n", Dis[t], Spend[t]);
}
return 0;
}
拓扑排序
Leagal or not
在一个 qq 群里有着许多师徒关系,如 A 是 B 的师父,同时 B 是 A 的徒弟,一个师父可能有许多徒弟,一个徒弟也可能会有许多不同的师父。
输入给出该群里所有的师徒关系,问是否存在这样一种非法的情况:以三个人为例,即 A 是 B 的师父,B 是 C 的师父,C 又反过来是 A 的师父。若我们将该群里的所有人都抽象成图上的结点,将所有的师徒关系都抽象成有向边(由师父指向徒弟),该实际问题就转化为一个数学问题——该图上是否存在一个环,即判断该图是否为有向无环图。
Input:
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.
样例输入:
3 2
0 1
1 2
2 2
0 1
1 0
0 0
样例输出:
YES
NO
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
vector<int> edge[501]; // 邻接链表,因为不存在权值,只需保存与其邻接的节点编号即可,所以vector中的元素为int
queue<int> Q; // 保存入度为0的节点的队列
int main() {
int inDegree[501]; // 统计每个节点的入度
int n, m;
while (scanf("%d%d", &n, &m) != EOF) {
if (n == 0 && m == 0)
break;
for (int i = 0; i < n; i++) { // 初始化所有节点,注意本题节点编号由0到n-1
inDegree[i] = 0; // 初始化入读信息,所有节点入度均为0
edge[i].clear(); // 清空邻接链表
}
while (m--) {
int a, b;
scanf ("%d%d", &a, &b); // 读入一条由a指向b的有向边
inDegree[b]++; // 累加节点b的入度
edge[a].push_back(b); // 将b加入a的邻接链表
}
// 若队列非空,则一直弹出队头元素,该操作的目的为清空队列中所有的元素(可能为上一组测试数据中的遗留数据)
while (!Q.empty())
Q.pop();
for (int i = 0; i < n; i++) {
if (inDegree[i] == 0)
Q.push(i); // 若节点入度为0,将其放入队列
}
int cnt = 0; // 计数器,初始值为0,用于累加已经确定拓扑序列的节点个数
while (!Q.empty()) {
// 读出队头节点编号,本例不需要求出确定的拓扑顺序,故不做处理,若要求求出确定的拓扑顺序,则将该节点紧接着放在已经确定的拓扑顺序后
int nowP = Q.front();
Q.pop();
cnt++;
for (int i = 0; i < edge[nowP].size(); i++) {
inDegree[edge[nowP][i]]--; // 去除某条边后,该边所指向后继节点入度减1
if (inDegree[edge[nowP][i]] == 0) { // 若该节点入度变为0
Q.push(edge[nowP][i]); // 将其放入队列中
}
}
}
if (cnt == n)
cout << "YES" << endl;
else
cout << "NO" << endl;
}
return 0;
}