一、并查集
并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中,其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。
例如:若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。 规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。
算法:
(1) 开始时,为每个人建立一个集合Set(x);
(2) 得到一个关系后a,b,合并相应集合Union(a,b);
题目描述:某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?
输入描述:
测试输入包含若干测试用例。每个测试用例的第1行给出两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。为简单起见,城镇从1到N编号。
注意:两个城市之间可以有多条道路相通,也就是说
3 3
1 2
1 2
2 1
这种输入也是合法的
当N为0时,输入结束,该用例不被处理。
输出描述:
对每个测试用例,在1行里输出最少还需要建设的道路数目。
示例输入:
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
#include
#include
using namespace std;
int roads[1001];
int main(){
while (true){
int n, m;
cin >> n;
if (n == 0){
return 0;
}
cin >> m;
for (int i = 1; i <= n; i++){
roads[i] = -1;
}
for (int i = 0; i < m; i++){
int a, b;
cin >> a >> b;
int max = roads[a] > roads[b] ? roads[a] : roads[b];
if (max == -1){
roads[a] = b;
}
else{
int ai = a;
int bi = b;
while (roads[ai] != -1){
ai = roads[ai];
}
while (roads[bi] != -1){
bi = roads[bi];
}
if (ai != bi){
roads[bi] = ai;
}
}
}
int sum = 0;
for (int i = 1; i <= n; i++){
if (roads[i] == -1){
sum++;
}
}
cout << sum - 1 << endl;
}
return 0;
}
二、最小生成树
根据Kruskal算法可以生成最小生成树,即每次选取最小权重的边,若不成环,则加入集合。而检测是否成环可以根据并查集进行计算。
题目描述:
某省调查乡村交通状况,得到的统计表中列出了任意两村庄间的距离。省政府“畅通工程”的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有直接的公路相连,只要能间接通过公路可达即可),并要求铺设的公路总长度为最小。请计算最小的公路总长度。
输入描述:
测试输入包含若干测试用例。每个测试用例的第1行给出村庄数目N ( < 100 );随后的N(N-1)/2行对应村庄间的距离,每行给出一对正整数,分别是两个村庄的编号,以及此两村庄间的距离。为简单起见,村庄从1到N编号。
当N为0时,输入结束,该用例不被处理。
输出描述:
对每个测试用例,在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
0
输出:
3
5
备注:
采用头文件中的sort函数,可以进行递增排序。
根据需要重载 < :
bool operator < (const Template &A) const{}
#include
#include
using namespace std;
int roads[101];
struct Node{
int a, b;
int cost;
bool operator < (const Node &A) const {
return cost < A.cost;
}
}node[5000];
int findroot(int a){
if (roads[a] == -1)
return a;
int i = a;
while (roads[i] != -1){
i = roads[i];
}
//roads[a] = i;
return i;
}
bool unionset(Node nd){
int aroot = findroot(nd.a);
int broot = findroot(nd.b);
if (aroot != broot){
roads[aroot] = broot;
return true;
}
else{
return false;
}
}
int main(){
while (true){
int n;
cin >> n;
if (n == 0){
return 0;
}
int times = n*(n - 1) / 2;
for (int i = 1; i <= n; i++){
roads[i] = -1;
}
for (int i = 0; i < times; i++){
cin >> node[i].a >> node[i].b >> node[i].cost;
}
sort(node,node+times);
int iter = 1;
int sum = 0;
for (int i = 0; iter < n; i++){
if (unionset(node[i])){
iter++;
sum += node[i].cost;
}
}
cout << sum << endl;
}
return 0;
}
三、最短路径
题目描述:
在每年的校赛里,所有进入决赛的同学都会获得一件很漂亮的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条商店到赛场的路线。
输出描述:
对于每组输入,输出一行,表示工作人员从商店走到赛场的最短时间
示例输入:
2 1
1 2 3
3 3
1 2 5
2 3 5
3 1 2
0 0
输出:
3
2
#include
using namespace std;
const int NOWAY = -1;
const int VNUM = 101;
// 邻接矩阵
int cost[VNUM][VNUM];
// Floyd算法
// cost[k][i][j] = min { cost[k-1][i][j],
// cost[k-1][i][k] + cost[kNOWAY][k][j] }
// 从i到j且中间节点最大不超过k
// 第一维数组可优化
void floyd(int n){
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]!=NOWAY && cost[k][j]!=NOWAY){
if (cost[i][j] == NOWAY || cost[i][j] > cost[i][k] + cost[k][j]){
cost[i][j] = cost[i][k] + cost[k][j];
}
}
}
}
}
if (cost[1][n] != NOWAY){
cout << cost[1][n] << endl;
}
}
// Dijkstra算法
// 从一个节点出发,不断迭代最短距离。
// 从length[]中选取最小长度对应的结点J,加入新结点j。
// 更新length[]。 a[k] = min { a[j] + cost[j][k] }
// 循环N次
bool mark[VNUM]; // 记录节点是否已加入
int length[VNUM];
void dijkstra(int n, int begin,int end){
for (int i = 1; i <= n; i++){
length[i] = -1;
mark[i] = false;
}
// 加入开始节点
mark[begin] = true;
length[begin] = 0;
int newp = begin;
// 不断加入新节点,循环加入n次
for (int i = 1; i <= n; i++){
for (int j = 1; j <= n; j++){
// 根据新加的节点的边,更新距离数组
if (mark[j] || cost[newp][j] == -1){
continue;
}
// 如果节点j不可达,但是newp可达;或者
// length[j]>length[newp]+cost[newp][j] 更新。
if (length[j]==-1||length[j]>length[newp]+cost[newp][j]){
length[j] = length[newp] + cost[newp][j];
}
}
// 寻找未加入的节点中最短路对应的节点,当作新加入的节点。
int min = 999999999;
for (int k = 1; k <= n; k++){
if (!mark[k] && length[k] != -1 && length[k] < min){
min = length[k];
newp = k;
}
}
mark[newp] = true;
}
cout << length[end] << endl;
}
int main(){
while (true){
int n, m;
cin >> n >> m;
if (n == 0 && m == 0){
return 0;
}
int a, b, c;
for (int i = 1; i <= n; i++){
for (int j = 1; j <= n; j++){
cost[i][j] = NOWAY;
}
cost[i][i] = 0;
}
for (int i = 0; i < m; i++){
cin >> a >> b >> c;
cost[a][b] = c;
cost[b][a] = c;
}
// floyd(n);
dijkstra(n, 1, n);
}
return 0;
}
四、拓扑排序
题目描述:
有N个比赛队(1<=N<=500),编号依次为1,2,3,。。。。,N进行比赛,比赛结束后,裁判委员会要将所有参赛队伍从前往后依次排名,但现在裁判委员会不能直接获得每个队的比赛成绩,只知道每场比赛的结果,即P1赢P2,用P1,P2表示,排名时P1在P2之前。现在请你编程序确定排名。
输入描述:
输入有若干组,每组中的第一行为二个数N(1<=N<=500),M;其中N表示队伍的个数,M表示接着有M行的输入数据。接下来的M行数据中,每行也有两个整数P1,P2表示即P1队赢了P2队。
输出描述:
给出一个符合要求的排名。输出时队伍号之间有空格,最后一名后面没有空格。
其他说明:
符合条件的排名可能不是唯一的,此时要求输出时编号小的队伍在前;输入数据保证是正确的,即输入数据确保一定能有一个符合要求的排名。
示例输入:
4 3
1 2
2 3
4 3
输出:
1 2 4 3
// 不断寻找入度为0的顶点。
#include
using namespace std;
const int VNUM = 501;
int cost[VNUM][VNUM];
int in[VNUM];
int ans[VNUM];
void topo(int n){
int index = 1;
for (int k = 0; k < n; k++){
for (int i = 1; i <= n; i++){
if (in[i] == 0){
ans[index] = i;
for (int j = 1; j <= n; j++){
if (cost[i][j] == 1){
in[j]--;
}
}
in[i] = -1;
index++;
break;
}
}
}
for (int i = 1; i < n; i++){
cout << ans[i] << " ";
}
cout <> n >> m){
int a, b;
for (int i = 1; i <= n; i++){
for (int j = 1; j <= n; j++){
cost[i][j] = 0;
}
in[i] = 0;
}
for (int i = 0; i < m; i++){
cin >> a >> b;
// 防止重边
if (cost[a][b] == 0)
in[b]++;
cost[a][b] = 1;
}
topo(n);
}
return 0;
}
五、广度优先搜索和深度优先搜索
问题描述:(胜利大逃亡)
Ignatius被魔王抓走了,有一天魔王出差去了,这可是Ignatius逃亡的好机会.
魔王住在一个城堡里,城堡是一个ABC的立方体,可以被表示成A个B*C的矩阵,刚开始Ignatius被关在(0,0,0)的位置,离开城堡的门在(A-1,B-1,C-1)的位置,现在知道魔王将在T分钟后回到城堡,Ignatius每分钟能从一个坐标走到相邻的六个坐标中的其中一个.现在给你城堡的地图,请你计算出Ignatius能否在魔王回来前离开城堡(只要走到出口就算离开城堡,如果走到出口的时候魔王刚好回来也算逃亡成功),如果可以请输出需要多少分钟才能离开,如果不能则输出-1.
输入:
输入数据的第一行是一个正整数K,表明测试数据的数量.每组测试数据的第一行是四个正整数A,B,C和T(1<=A,B,C<=50,1<=T<=1000),它们分别代表城堡的大小和魔王回来的时间.然后是A块输入数据(先是第0块,然后是第1块,第2块......),每块输入数据有B行,每行有C个正整数,代表迷宫的布局,其中0代表路,1代表墙.(如果对输入描述不清楚,可以参考Sample Input中的迷宫描述,它表示的就是上图中的迷宫)
特别注意:本题的测试数据非常大,请使用scanf输入,我不能保证使用cin能不超时.在本OJ上请使用Visual C++提交.输出:
对于每组测试数据,如果Ignatius能够在魔王回来前离开城堡,那么请输出他最少需要多少分钟,否则输出-1.Sample Input
1
3 3 4 20
0 1 1 1
0 0 1 1
0 1 1 1
1 1 1 1
1 0 0 1
0 1 1 1
0 0 0 0
0 1 1 0
0 1 1 0Sample Output
11
解题思路:DFS,每次向六个方向扩展,时间加1,采用queue数据结构,第一次遍历到出口 时即最短时间。
问题:第一段是网上答案,第二段是我的代码。我的代码一直出现内存超限,暂未找到原因。
#include
#include
#include
#include
#include
#include
#include
using namespace std;
bool mark[50][50][50];
bool m[50][50][50];
int t;
int go[][3] =
{
1, 0, 0,
-1, 0, 0,
0, 1, 0,
0, -1, 0,
0, 0, 1,
0, 0, -1
};
struct e
{
int x, y, z;
int t;
};
queue q;
int bfs(int a, int b, int c)
{
while (!q.empty())
{
e now;
now = q.front();
q.pop();
if (now.t >= t) {
return -1;
}
for (int i = 0; i<6; i++)
{
int nx = now.x + go[i][0];
int ny = now.y + go[i][1];
int nz = now.z + go[i][2];
if (nx<0 || nx >= a || ny<0 || ny >= b || nz<0 || nz >= c) continue;
if (mark[nx][ny][nz] == 1) continue;
if (m[nx][ny][nz] == 1) continue;
m[nx][ny][nz] = 1;
e tmp;
tmp.x = nx;
tmp.y = ny;
tmp.z = nz;
tmp.t = now.t + 1;
if (tmp.t
#include
#include
using namespace std;
const int MAX = 50;
int map[MAX][MAX][MAX];
bool mark[MAX][MAX][MAX];
int A, B, C, T;
struct Dot{
int a;
int b;
int c;
int t;
};
queue Q;
void BFS(){
while (Q.empty() == false){
Dot dot = Q.front();
int a = dot.a;
int b = dot.b;
int c = dot.c;
int t = dot.t;
if (a == A - 1 && b == B - 1 && c == C - 1 && !mark[a][b][c]){
mark[a][b][c] = true;
if (t <= T)
cout << t << endl;
else
cout << -1 << endl;
return;
}
if (t > T){
cout << -1 << endl;
return;
}
mark[a][b][c] = true;
Dot temp;
if (a > 0 && !mark[a - 1][b][c] && map[a - 1][b][c] == 0){
temp.t = t + 1;
temp.a = a - 1;
temp.b = b;
temp.c = c;
Q.push(temp);
}
if (a 0 && !mark[a][b - 1][c] && map[a][b - 1][c] == 0){
temp.t = t + 1;
temp.a = a;
temp.b = b-1;
temp.c = c;
Q.push(temp);
}
if (b 0 && !mark[a][b][c - 1] && map[a][b][c - 1] == 0){
temp.t = t + 1;
temp.a = a;
temp.b = b;
temp.c = c-1;
Q.push(temp);
}
if (c> map[i][j][k];
scanf_s("%d", &map[i][j][k]);
mark[i][j][k] = 0;
}
}
}
while (!Q.empty()){
Q.pop();
}
Dot d;
d.a = 0;
d.b = 0;
d.c = 0;
d.t = 0;
map[0][0][0] = 0;
Q.push(d);
BFS();
if (!mark[A - 1][B - 1][C - 1])
cout << -1 << endl;
}
return 0;
}
Problem Description(非常可乐)
大家一定觉的运动以后喝可乐是一件很惬意的事情,但是seeyou却不这么认为。因为每次当seeyou买了可乐以后,阿牛就要求和seeyou一起分享这一瓶可乐,而且一定要喝的和seeyou一样多。但seeyou的手中只有两个杯子,它们的容量分别是N 毫升和M 毫升 可乐的体积为S (S<101)毫升 (正好装满一瓶) ,它们三个之间可以相互倒可乐 (都是没有刻度的,且 S==N+M,101>S>0,N>0,M>0) 。聪明的ACMER你们说他们能平分吗?如果能请输出倒可乐的最少的次数,如果不能输出"NO"。Input
三个整数 : S 可乐的体积 , N 和 M是两个杯子的容量,以"0 0 0"结束。Output
如果能平分的话请输出最少要倒的次数,否则输出"NO"。Sample Input
7 4 3
4 1 3
0 0 0Sample Output
NO
3
#include
#include
using namespace std;
struct Liquid{
int n;
int m;
int s;
int times;
};
bool mark[101][101][101];
queue Q;
int N, M, S;
void AtoB(int &a, int &b, int max){
if (a + b > max){
a = a + b - max;
b = max;
}
else{
b = a + b;
a = 0;
}
}
void push_back(int n, int m, int s, int t){
if (n > N || m > M || s > S){
return;
}
Liquid next ;
if (mark[n][m][s] == false){
next.n = n;
next.m = m;
next.s = s;
next.times = t + 1;
Q.push(next);
}
}
int main(){
while (true){
cin >> S >> N >> M;
bool flag = false;
if (S == 0 && N == 0 && M == 0){
return 0;
}
if (S % 2 != 0){
cout << "NO" << endl;
continue;
}
for (int i = 0; i <= N; i++){
for (int j = 0; j <= M; j++){
for (int k = 0; k <= S; k++)
mark[i][j][k] = 0;
}
}
while (!Q.empty()){
Q.pop();
}
Liquid temp;
temp.n = 0;
temp.m = 0;
temp.s = S;
temp.times = 0;
Q.push(temp);
while (Q.empty() == false){
Liquid now = Q.front();
Liquid next;
Q.pop();
int n, m, s, t;
n = now.n;
m = now.m;
s = now.s;
t = now.times;
mark[n][m][s] = true;
if ((n == S / 2 && m == S / 2) || (n == S / 2 && s == S / 2) || (m == S / 2 && s == S / 2)){
cout << t << endl;
flag = true;
break;
}
AtoB(n, m, M);
push_back(n, m, s, t);
n = now.n;
m = now.m;
s = now.s;
t = now.times;
AtoB(n, s, S);
push_back(n, m, s, t);
n = now.n;
m = now.m;
s = now.s;
t = now.times;
AtoB(m, n, N);
push_back(n, m, s, t);
n = now.n;
m = now.m;
s = now.s;
t = now.times;
AtoB(m, s, S);
push_back(n, m, s, t);
n = now.n;
m = now.m;
s = now.s;
t = now.times;
AtoB(s, m, M);
push_back(n, m, s, t);
n = now.n;
m = now.m;
s = now.s;
t = now.times;
AtoB(s, n, N);
push_back(n, m, s, t);
}
if (flag)
continue;
cout << "NO" << endl;
}
return 0;
}
此类题目应先确认状态的转移关系。
(x,y,z...) ---> (xx,yy,zz...).。
其次找清判断结果的条件。
六、A*算法(拓展)
综合了DFS/BFS和启发式寻路算法。