【图论习题集】(dijkstra, 匈牙利算法,更新至8873字)

目录

板块一:最短路

第一题:畅通工程续(dijkstra的堆优化)

第二题:HDU today(普通dijkstra + map)

第三题:choose the best route(普通dijkstra + 多起点 + 有向图)

板块二:二分图

第一题:过山车(匈牙利算法基本模板)

第二题:机器调度(普通匈牙利算法,最小点覆盖)

第三题:Air Raid(最小路径覆盖)

第四题:50 years, 50 colors(比较抽象的匈牙利算法,补充邻接矩阵写法)

第五题:courses(简单题,复习巩固)

第六题:男女舞会


板块一:最短路

第一题:畅通工程续(dijkstra的堆优化)

 hdoj1874

前言:dijkstra有两种,直接上二维邻接矩阵的普通算法,和堆优化的算法。

堆优化大凡有两种,链式前向星和邻接表,前者好写(但是博主觉得不好写。)

其实可以直接用vector<> a[]存邻接表,这样好写,很多时候我们都会用这个来存图。

dijkstra的思路就是不断地查找最小距离并更新,详见另外的博文。

代码:

#include <bits/stdc++.h> 
#define inf 0x3f3f3f3f
using namespace std;
const int maxn = 1e5;
typedef pair<int,int> cp;
vector<cp> g[maxn];
int d[maxn];bool vis[maxn];
struct  node
{
    int id,val;
    node() {
	};
    node(int a,int b) {
    	id = a;
    	val = b;
	};
    bool operator <(node A) const{
        return A.val < val;
    };//bool operator node a const 不知道为什么用了friend的wa了
};
void dijkstra(int u) {
	priority_queue<node> q;
	q.push(node(u, 0));//推入
	node temp;
	while (!q.empty()) {
		temp = q.top();
		q.pop();
		int x = temp.id;
		if (vis[x]) {
			continue;
		}
		vis[x] = 1;//标记最小点,防止重复更新,因为队列里面还存在历史距离,就是更新之前的点
		for (int i = 0; i < g[x].size(); i++) {//以之前的最小距离的点更新
			int y = g[x][i].first, dis = g[x][i].second;
			if (!vis[y] && d[y] > d[x] + dis) {
				d[y] = d[x] + dis;/*更新未标记而且更新点的距离点*/
				q.push(node(y, d[y]));//推入,
			}
		}
	}
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	int n, m, f, t, dis, a, b;
	while (cin >> n >> m) {
		for (int i = 0; i <= n; i++) {
			g[i].clear();//初始化
		}
		memset(vis, 0, sizeof(vis));
		memset(d, 0x3f, sizeof(d));
		for (int i = 1; i <= m; i++) {
			cin >> f >> t >> dis;
			g[f].push_back(cp(t, dis));//无向图
			g[t].push_back(cp(f, dis));
		}
		cin >> a >> b;
		d[a] = 0;
		dijkstra(a);
		if (d[b] == inf) {
			cout << -1 << '\n';
		} else {
			cout << d[b] << '\n'; 
		}
	}
	return 0;
}

第二题:HDU today(普通dijkstra + map)

hdoj2112

闲言:这道貌似好像没有直接说是无向图,不过根据经验公交车应该是双向往返的。还有关于map的细节,.count的类名是返回0或者1,其实也可以直接用键值索引,如果说之前没有被赋值的话默认是1,不过有时候如果赋值后也是零可能就有问题了。另外,unordered_map的本质是哈希表,查找速度很快,不过据说在cf上容易被hack,建议自制hash表。这个的内存占用比map多,但是速度快。这里有一个坑点,就是用完一次要注意mp的清零,因为这会影响节点数p的判断,导致wa。

下面是代码:

#include <bits/stdc++.h>
#include <tr1/unordered_map>
#define inf 0x3f3f3f3f
using namespace std;
tr1::unordered_map<string, int> mp;//注意库和前缀
const int N = 200;
int g[N][N], ed, vis[N], dis[N], p;//g是邻接矩阵,ed是终点,vis是访问标记,p是节点数加1
void dj() {
	memset(vis, 0, sizeof(vis));//标记清零
	for (int i = 1; i <= p; i++) {
		dis[i] = (i == 1 ? 0 : inf);
	}//初始化距离
	for (int i = 1; i <= p; i++) {
		int m = inf, x;
		for (int j = 1; j <= p; j++) {
			if (dis[j] < m && !vis[j]) {
				m = dis[j];
				x = j;//找到最小的边
			}
		}
		vis[x] = 1;//标记最小的边
		for (int y = 1; y <= p; y++) {
			dis[y] = min(dis[y], dis[x] + g[x][y]);
		}//更新距离
	}
	if (dis[ed] == inf) {
		cout << -1 << '\n';
	} else {
		cout << dis[ed] << '\n'; 
	}
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	int t;
	string str1, str2;
	int l;
	while (cin >> t, t != -1) {
		mp.clear();//要小心
		p = 1;
		cin >> str1 >> str2;
		mp[str1] = p++;
		if (str2 != str1) {//如果mp是非空的话
			mp[str2] = p++;//会导致下面的p错乱
			ed = 2;
		} else {
			ed = 1;
		}
		memset(g, 0x3f, sizeof(g));
		for (int i = 0; i < t; i++) {
			cin >> str1 >> str2 >> l;
			if (!mp[str1]) {
				mp[str1] = p++;
	    	}
	    	if (!mp[str2]) {
	    		mp[str2] = p++;
			}
			g[mp[str1]][mp[str2]] = min(g[mp[str1]][mp[str2]], l);//多边取最小
			g[mp[str2]][mp[str1]] = g[mp[str1]][mp[str2]];//双向
		}
		for (int i = 1; i <= p; i++) {
				g[i][i] = 0;
		}
		dj(); 
	}
	return 0; 
}

第三题:choose the best route(普通dijkstra + 多起点 + 有向图)

hdoj2680

注意:是有向的,我们把起点集合里的第一个点作为起点。

题目似乎是有点含糊,from p to q应该来说是有向的。

#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
using namespace std;
const int N = 1005;
int mp[N][N], d[N], vis[N], n, s, st[N]; //st是起点集合
void init() {//初始化,vis,mp
	memset(vis, 0, sizeof(vis));
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n; j++) {
			mp[i][j] = (i == j ? 0 : inf);
		}
	}
}
void dijkstra() {
		for (int i = 1; i <= n; i++) {
		d[i] = (i == st[0] ? 0 : inf);
	}//初始化d
	for (int i = 1; i <= n; i++) {
		int m = inf, x;
		for (int j = 1; j <= n; j++) {
			if (!vis[j] && d[j] < m) {
				m = d[j];
				x = j;
			}
		}
		vis[x] = 1;
		for (int y = 1; y <= n; y++) {
			d[y] = min(d[y], d[x] + mp[x][y]);
		}
	}
	if (d[s] == inf) {
		cout << -1 << '\n';
	} else {
		cout << d[s] << '\n';
	}
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	int m;
	while (cin >> n >> m >> s) {
		init();
		int a, b, c;
		while (m--) {
			cin >> a >> b >> c;
			mp[a][b] = min(mp[a][b], c);//有向
		}
		int w;
		cin >> w;
		for (int i = 0; i < w; i++) {
			cin >> st[i];
				mp[st[0]][st[i]] = 0;//!!!注意了
		}
		dijkstra();
	}
	return 0;
}

板块二:二分图

第一题:过山车(匈牙利算法基本模板)

hdoj2063

前言:匈牙利算法的本质是DFS,就用那个男女朋友的例子为例,每个女生暗恋一个男生,我们先是假定第一个女生和他暗恋的第一个男生配对,接下来,我们考察第二个女生,如果这个女生也喜欢这个男生,那么我们先考虑第一位女生让步,让她取希望暗恋的第二个男生,如果成功,那么配对的数目自然就加一了,或者没有其他男生的话,只能让第二个女生去喜欢暗恋的第二个男生,一次类推。

其实个人觉得这么有着动态规划和贪心的想法,利用DFS我们考察以每个女生为起点的关系。DFS的每一层递归里面存储了某个女生的对暗恋的男生中位次。每当考虑下一个女生,为了使得后来的女生得到更大的选择空间,我们优先考虑,能否让这个女生选择位次更小的男生,因此我们逐个更改历史数据,“挤”出可以挤出的空间,优先给她,其次是考虑新的选择。这样可以让后来者的选择空间最大化,从而使得整体最优。

当然,严谨的证明是通过增广路证明的,当无法找到增广路时就是该二分图的最大匹配数。

下面是代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 505;
int n, m, cp[N];
bool vis[N];
vector<int> a[N];
bool dfs(int x) {//深度优先搜索判断能否找到增广路
	for (int i = 0; i < a[x].size(); i++) {//i存储的某女生中所有暗恋的男生中男生的位次
		int y = a[x][i];
		if (!vis[y]) {//防止在某一次的深搜的过程中重复使用某个男生
			vis[y] = 1;
			if (cp[y] == 0 || dfs(cp[y])) {/*这是核心的地方,只有当y没有配对,或者如果y有配对的话,我们会依次考察所有先前涉及冲突的女生,考虑是否能够调换,即查找增广路径,一直搜到有一个空余的位置为止, 如果历史数据里面无法找到一条增广路径,那么只能i++,当前女生选择下一个男生*/
				cp[y] = x;//如果直接找到未配对,或者有增广路径,最终找到一未配对的
				return 1;//我们就,逐级配对,总那个没有配对的男生和最后一个考察的女生配对
			}//依次配对,递归出口就在if出,最后将新增的和挪出来的男生配对
		}
	}
	return 0;
}
int hungarian() {//匈牙利算法
	int cnt = 0;
	for (int i = 1; i <= n; i++) {//我们要考虑所有的女生能否加入
		memset(vis, 0, sizeof(vis));
		if (dfs(i)) {
			cnt++;
		}
	}
	return cnt;
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	int k;
	while (cin >> k, k) {
		cin >> n >> m;
		int g, b;
		for (int i = 1; i <= n; i++) {
			a[i].clear();//清空数组
		}
		for (int i = 1; i <= k; i++) {
			cin >> g >> b;
			a[g].push_back(b);//存图,同起点
		}
		memset(cp, 0, sizeof(cp));
		int ans = hungarian();
		cout << ans << '\n';
	}
	return 0;
}

一些想法:

1、最大匹配的情况不一定是唯一的。

A      a

B

C

比如三个人都喜欢a,那么有三种匹配的情况。

但是最大的匹配数是一样的。

都是1;

2、我们这里是取第一个女生优先。取的顺序是否会影响结果呢?

因为对于仅考虑了第一个女生的时候我们得到的就是当前情况下的最大匹配数。

由于增广路径的正确性,我们得到的每一个都是当前女生数的最大匹配数,这里运用的动态规划的思想。

第二题:机器调度(普通匈牙利算法,最小点覆盖)

hdoj1150

芝士:最小点覆盖=最大匹配数

注意初始化的位置。

代码

#include <bits/stdc++.h>
using namespace std;
const int N = 105;
vector<int> g[N];
int vis[N], n, cp[N];
bool dfs(int x) {
	for (int i = 0; i < g[x].size(); i++) {
		int y = g[x][i];
		if (!vis[y]) {
			vis[y] = 1;
			if (cp[y] == -1 || dfs(cp[y])) {
				cp[y] = x;
				return 1;
			}
		}
	}
	return 0;
}
int hungarian() {
	int ans = 0;
	for (int i = 0; i < n; i++) {
		memset(vis, 0, sizeof(vis));
		if (dfs(i)) {
			ans++;
		}
	} 
	return ans;
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	int m, k;
	while (cin >> n, n) {
		for (int i = 1; i <= n; i++) {
		g[i].clear();//初始化
	}
		memset(cp, -1, sizeof(cp));//模式从0开始
		cin >> m >> k;
		int a, b, c;
		while (k--) {
			cin >> a >> b >> c;
			if (!b || !c) {//0不必修改模式
				continue;
			}
			g[b].push_back(c);
		}
		cout << hungarian() << '\n';
	}
	return 0;
}

第三题:Air Raid(最小路径覆盖)

hdoj1151

每个独立的点都是一条路,每有一条边相连,那么路就减少一条,当最大匹配数是最小,这里不能相交。

模板几乎是一模一样的,没有什么差别。

最小路径覆盖=所有的点数-最大匹配数

代码如下:

#include <bits/stdc++.h>
using namespace std;
int n;
const int N = 125;
vector<int> g[N];
int vis[N], cp[N];
bool dfs(int x) {
	for (int i = 0; i < g[x].size(); i++) {
		int y = g[x][i];
		if (!vis[y]) {
			vis[y] = 1;
			if (cp[y] == 0 || dfs(cp[y])) {
				cp[y] = x;
				return 1;
			}
		}
	}
	return 0;
}
int hungarian() {
	int ans = 0; 
	for (int i = 1; i <= n; i++) {
		memset(vis, 0, sizeof(vis));
		if (dfs(i)) {
			ans++;
		}
	}
	return ans;
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	int t;
	cin >> t;
	while (t--) {
		memset(cp, 0, sizeof(cp));
		int m;
		cin >> n >> m;
		for (int i = 1; i <= n; i++) {
			g[i].clear();
		}
		int a, b;
		while (m--) {
			cin >> a >> b;
			g[a].push_back(b);
		}
		cout << n - hungarian()<< '\n';
	}
	return 0;
}

第四题:50 years, 50 colors(比较抽象的匈牙利算法,补充邻接矩阵写法)

hdoj1498

前言:这些题目居然把匈牙利算法藏得这么深。这里将每一行,每一列抽象为二分图的点,如果要求最小的次数,这里我们要选择最少行和列,也就是选出二分图里最少的点,这便是最小点覆盖。

把二维的坐标,变成了01关系的二分图,十分奇妙。

下面是代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 105;
int mp[N][N], n, book[55], color, cp[N], vis[N];//mp邻接矩阵,color是当前判断颜色
bool dfs(int x) {
	for (int i = 1; i <= n; i++) {
		if (!vis[i] && mp[x][i] == color) {//有需要的的边,且未访问过
			vis[i] = 1;
			if (cp[i] == 0 || dfs(cp[i])) {
				cp[i] = x;
				return 1;
			}
		}
	}
	return 0;
}
int hungarian() {
	int ans = 0;
	for (int i = 1; i <= n; i++) {
		memset(vis, 0, sizeof(vis));
		if (dfs(i)) {
			ans++;
		}
	}
	return ans;
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	int k;
	while (cin >> n >> k, n + k) {
		memset(book, 0, sizeof(book));
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= n; j++) {
				cin >> mp[i][j];
				book[mp[i][j]]++;//bool数组即可
			}
		}
		int flag = 0;
		for (int i = 1; i <= 50; i++) {
			memset(cp, 0, sizeof(cp));
			if (book[i]) {
				color = i;
				if (hungarian() > k) {//如果最小点覆盖数大于最大可用次数
					if (flag) {
						cout << ' ';
					}
					cout << i;//表明这个颜色不行。
					flag = 1;
				}
			}
		}
		if (!flag) {
			cout << -1;
		}
		cout << '\n';
	}
	return 0;
} 

第五题:courses(简单题,复习巩固)

hdoj1083

代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
vector<int> g[N];
int vis[N], cp[N], t, n;
bool dfs(int x) {
	for (int i = 0; i < g[x].size(); i++) {
		int y = g[x][i];//查找增广路 
		if (!vis[y]) {
			vis[y] = 1;
			if (cp[y] == -1 || dfs(cp[y])) {
				cp[y] = x;
				return 1;
			}
		}
	}
	return 0;
}
int hungarian() {//匈牙利算法 
	memset(cp, -1, sizeof(cp));
	int ans = 0;
	for (int i = 0; i < t; i++) {//对每个左端点 
		memset(vis, 0, sizeof(vis));
		if (dfs(i)) {
			ans++;
		}
	}
	return ans;
}
int main() {
	int tt;//是样例的数目 
	cin >> tt;
	while (tt--) {
		cin >> t >> n;//t是左点集,n是右点集 
		for (int i = 0; i < t; i++) {
			g[i].clear();//左点集映射清空 
		}
		for (int i = 0; i < t; i++) {//对每个左端点 
			int a, b;
			cin >> a;//是映射数目 
			for (int j = 0; j < a; j++) {
				cin >> b;
				g[i].push_back(b);//存右端点 
			} 
		}
		if (hungarian() >= t) {//最大匹配数大于等于课程数 
			cout << "YES\n";
		} else {
			cout << "NO\n";
		}
	}
	return 0;

第六题:男女舞会

前言,这是类似田忌赛马的情况,我们需要建立二分图,然后才能进行匈牙利算法,再次看到这个算法又有了一点新的体会,匈牙利算法为什么时间复杂度低,因为,在DFS的过程中不回溯,像走迷宫的话要复原,如果找到了,取反,如果找不到说明这是死胡同,因为二分图的连线是一一对应的,vis[y] = 1的剪枝力度很大。第一层的时候如果找到一个没有访问过的点,但是有cp进入下一层,一直到最后,vis的标记使得不会走回头路,构造新的增广路中这些点都用过了,如果到最后都没有找到,递归到上一层找下一个可以用的点,注意,最后一个点的情况,要么有未配对,没有其他点可以选。查找完之后还标记过的点,是终结点,一定是死路(类似SG函数里面的必败点)

#include <bits/stdc++.h>
using namespace std;
const int N = 105;
vector<int> g[N];
int n, m, cp[N] = {0}, a[N] = {0};
bool vis[N] = {0};
bool dfs(int x) {
	for (int i = 0; i < g[x].size(); i++) {
		int y = g[x][i];
		if (!vis[y]) {
			vis[y] = 1;
			if (cp[y] == 0 || dfs(cp[y])) {
				cp[y] = x;
				return 1;
			}
		}
	}
	return 0;
}
int hungarian() {
	int cnt = 0;
	for (int i = 1; i <= n; i++) {
		memset(vis, 0, sizeof(vis));
		if (dfs(i)) {
			cnt++;
		}
	}
	return cnt;
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	cin >> m;
	for (int i = 1; i <= m; i++) {
		int c;
		cin >> c;
		for (int j = 1; j <= n; j++) {
			if (abs(a[j] - c) <= 1) {
				g[j].push_back(i);
			}
		}
	}
	cout << hungarian() << '\n';
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值