欧拉回路、链路、其他边覆盖问题

目录

一,欧拉回路、欧拉链路

1,无向图

2,有向图

二,Hierholzer算法

1,递归版

力扣 332. 重新安排行程(有向图的欧拉链路)

力扣 753. 破解保险箱(有向图的欧拉回路)

2,非递归版

力扣 2097. 合法重新排列数对(有向图的欧拉回路或链路)

三,链路覆盖问题

1,链路覆盖问题

2,相关猜想

3,相关定理

4,应用

四,回路覆盖问题

1,回路覆盖问题

2,相关猜想

3,最小权值回路覆盖

4,k重回路覆盖问题

五,中国邮递员问题

1,中国邮递员问题

2,中国邮递员问题 VS 回路覆盖问题


一,欧拉回路、欧拉链路

从起点A出发,不重复的走完所有的边,最终回到A,得到的回路叫欧拉回路。

从起点A出发,不重复的走完所有的边,到达终点B,得到的链路叫欧拉链路。

寻找欧拉回路或者欧拉链路就是经典的一笔画问题。

1,无向图

对于无向连通图,存在欧拉回路的充要条件是奇点数量为0,存在欧拉链路的充要条件是奇点数量为2。

换句话说,如果奇点数量大于2则无法一笔画。

2,有向图

对于有向图G,存在欧拉回路的充要条件是,G是强连通图且所有节点的入度都等于出度。

对于有向图G,存在欧拉链路的充要条件是,G是半连通图且只有2个节点的出度-入度=正负1,其他节点都满足入度都等于出度。

二,Hierholzer算法

1,递归版

Hierholzer算法求欧拉回路或链路就是从任意一点开始进行dfs遍历所有的边,把遍历结束的点入栈,最终得到的就是欧拉回路,栈顶就是起点。

class Hierholzer {
public:
	stack<int>euler;//欧拉回路或链路,栈顶是起点
	Hierholzer(int n, map<int, vector<int>>& m,int type,int start=0)//type=0是无向图 1是有向图
	{
		this->n = n;
		this->m = m;
		this->type = type;
		dfs(GetStartPoint(start));
	}
private:
	int GetStartPoint(int start)//链路是唯一起点,回路是指定起点
	{
		if (type == 0) {
			for (auto& mi : m) {
				if (mi.second.size() % 2)return mi.first;
				for (auto nk : mi.second)num[id(mi.first, nk)]++;
			}
			for (auto& ni : num)ni.second /= 2;
		} else {
			map<int, int>m2;
			for (auto& mi : m)for (auto nk : mi.second)m2[nk]++, num[id(mi.first, nk)]++;
			for (auto& mi : m)if (mi.second.size() > m2[mi.first])return mi.first;
		}
		return start;
	}
	void dfs(int k)
	{
		for (auto nk : m[k]) {
			if (num[id(k, nk)]--<=0)continue;
			dfs(nk);
		}
		euler.push(k);
	}
	long long id(int a, int b)
	{
		if (type == 0 && a > b)a ^= b ^= a ^= b;
		return (long long)a * n + b;
	}
	int n;
	int type;
	map<int, vector<int>> m;//邻接表
	map<long long, int>num;//支持多重边
};

这份代码能同时适用于无向图和有向图,自动判断是求欧拉回路还是链路,并以栈作为返回值。

另外一个优点是支持多重边。

缺点是没有做校验是否存在欧拉回路或链路。

力扣 332. 重新安排行程(有向图的欧拉链路)

给你一份航线列表 tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。

所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。

  • 例如,行程 ["JFK", "LGA"] 与 ["JFK", "LGB"] 相比就更小,排序更靠前。

假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。

示例 1:

输入:tickets = [["MUC","LHR"],["JFK","MUC"],["SFO","SJC"],["LHR","SFO"]]
输出:["JFK","MUC","LHR","SFO","SJC"]

示例 2:

输入:tickets = [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]
输出:["JFK","ATL","JFK","SFO","ATL","SFO"]
解释:另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"] ,但是它字典排序更大更靠后。

提示:

  • 1 <= tickets.length <= 300
  • tickets[i].length == 2
  • fromi.length == 3
  • toi.length == 3
  • fromi 和 toi 由大写英文字母组成
  • fromi != toi

题意:

给一个带多重边的有向图,指定起点,求欧拉链路。

class Solution {
public:
	vector<string> findItinerary(vector<vector<string>>& tickets) {
		vector<string>v;
		map<string, int>m;
		for (auto& ti : tickets)m[ti[0]],m[ti[1]];
		for (auto& mi : m)v.push_back(mi.first);
		sort(v.begin(), v.end());
		m.clear();
		for (int i = 0; i < v.size(); i++)m[v[i]] = i;
		map<int, vector<int>>g;
		for (auto& ti : tickets)g[m[ti[0]]].push_back(m[ti[1]]);
		for (auto& gi : g)sort(gi.second.begin(), gi.second.end());
		stack<int>s = Hierholzer(v.size(), g, 1, m["JFK"]).euler;
		vector<string> ans;
		while (!s.empty())ans.push_back(v[s.top()]), s.pop();
		return ans;
	}
};

力扣 753. 破解保险箱(有向图的欧拉回路)

有一个需要密码才能打开的保险箱。密码是 n 位数, 密码的每一位是 k 位序列 0, 1, ..., k-1 中的一个 。

你可以随意输入密码,保险箱会自动记住最后 n 位输入,如果匹配,则能够打开保险箱。

举个例子,假设密码是 "345",你可以输入 "012345" 来打开它,只是你输入了 6 个字符.

请返回一个能打开保险箱的最短字符串。

示例1:

输入: n = 1, k = 2
输出: "01"
说明: "10"也可以打开保险箱。
 

示例2:

输入: n = 2, k = 2
输出: "00110"
说明: "01100", "10011", "11001" 也能打开保险箱。
 

提示:

n 的范围是 [1, 4]。
k 的范围是 [1, 10]。
k^n 最大可能为 4096。

思路:

以前n-1位密码的所有可能性作为点,以最后一位作为边,那么保险箱的枚举密码就可以表示成求欧拉回路或链路。

节点数量是pow(k, n - 1),边的数量是pow(k, n)

实际上,由于对称性,求的是欧拉回路。

class Solution {
public:
	string crackSafe(int n, int k) {
		if (n == 1) {
			string str(k, '0');
			for (int i = 0; i < str.size(); i++)str[i] += i;
			return str;
		}
		map<int, vector<int>>m;
		int num = pow(k, n - 1);
		for (int i = 0; i < num; i++) {
			for (int j = 0; j < k; j++)m[i].push_back(i % (num / k)*k + j);
		}
		auto s = Hierholzer(m.size(), m, 1).euler;
		string str(num * k + n - 1, '0');
		int id = 0, t = s.top();
		char c = '0';
		s.pop();
		for (int i = 0; i < n - 1; i++)str[id++] = c + s.top() % k, t /= k;
		while (!s.empty())str[id++] = c + s.top() % k, s.pop();
		return str;
	}
};

2,非递归版

为了克服递归太深导致爆栈的问题,我把算法改写成非递归版,功能和接口都不变。

class Hierholzer {
public:
	stack<int>euler;//欧拉回路或链路,栈顶是起点
	Hierholzer(int n, map<int, vector<int>>& m,int type,int start=0)//type=0是无向图 1是有向图
	{
		this->n = n;
		this->m = m;
		this->type = type;
		dfs(GetStartPoint(start));
	}
private:
	int GetStartPoint(int start)//链路是唯一起点,回路是指定起点
	{
		if (type == 0) {
			for (auto& mi : m) {
				if (mi.second.size() % 2)return mi.first;
				for (auto nk : mi.second)num[id(mi.first, nk)]++;
			}
			for (auto& ni : num)ni.second /= 2;
		} else {
			map<int, int>m2;
			for (auto& mi : m)for (auto nk : mi.second)m2[nk]++, num[id(mi.first, nk)]++;
			for (auto& mi : m)if (mi.second.size() > m2[mi.first])return mi.first;
		}
		return start;
	}
	void dfs(int k)
	{
		while (true) {
			while (mid[k] < m[k].size()) {
				if (num[id(k, m[k][mid[k]])]-- <= 0)mid[k]++;
				else sdfs.push(k), k = m[k][mid[k]];
			}
			euler.push(k);
			if (sdfs.empty()) return;
			k = sdfs.top(), sdfs.pop();
		}
	}
	inline long long id(int a, int b)
	{
		if (type == 0 && a > b)a ^= b ^= a ^= b;
		return (long long)a * n + b;
	}
	int n;
	int type;
	stack<int>sdfs;
	map<int, vector<int>> m;//邻接表
	map<int, int>mid;
	map<long long, int>num;//支持多重边
};

力扣 2097. 合法重新排列数对(有向图的欧拉回路或链路)

给你一个下标从 0 开始的二维整数数组 pairs ,其中 pairs[i] = [starti, endi] 。如果 pairs 的一个重新排列,满足对每一个下标 i ( 1 <= i < pairs.length )都有 endi-1 == starti ,那么我们就认为这个重新排列是 pairs 的一个 合法重新排列 。

请你返回 任意一个 pairs 的合法重新排列。

注意:数据保证至少存在一个 pairs 的合法重新排列。

示例 1:

输入:pairs = [[5,1],[4,5],[11,9],[9,4]]
输出:[[11,9],[9,4],[4,5],[5,1]]
解释:
输出的是一个合法重新排列,因为每一个 endi-1 都等于 starti 。
end0 = 9 == 9 = start1 
end1 = 4 == 4 = start2
end2 = 5 == 5 = start3

示例 2:

输入:pairs = [[1,3],[3,2],[2,1]]
输出:[[1,3],[3,2],[2,1]]
解释:
输出的是一个合法重新排列,因为每一个 endi-1 都等于 starti 。
end0 = 3 == 3 = start1
end1 = 2 == 2 = start2
重新排列后的数组 [[2,1],[1,3],[3,2]] 和 [[3,2],[2,1],[1,3]] 都是合法的。

示例 3:

输入:pairs = [[1,2],[1,3],[2,1]]
输出:[[1,2],[2,1],[1,3]]
解释:
输出的是一个合法重新排列,因为每一个 endi-1 都等于 starti 。
end0 = 2 == 2 = start1
end1 = 1 == 1 = start2

提示:

  • 1 <= pairs.length <= 105
  • pairs[i].length == 2
  • 0 <= starti, endi <= 109
  • starti != endi
  • pairs 中不存在一模一样的数对。
  • 至少 存在 一个合法的 pairs 重新排列。
class Solution {
public:
	vector<vector<int>> validArrangement(vector<vector<int>>& pairs) {
		map<int, vector<int>>g;
		for (auto& p : pairs)g[p[0]].push_back(p[1]);
		stack<int>s = Hierholzer(g.size(), g, 1, pairs[0][0]).euler;
		int a = s.top(), b;
		s.pop();
		vector<vector<int>>ans(s.size());
		for(int i=0;i<ans.size();i++) {
			b = s.top();
			ans[i] = vector<int>{ a, b };
			a = b;
			s.pop();
		}
		return ans;
	}
};

三,链路覆盖问题

1,链路覆盖问题

寻找一组链路,使得任意边至少被经过1次,至多被经过k次。

2,相关猜想

猜想一,n个点的连通图,可以被n/2条链路覆盖。

猜想二,存在一个小于n/2的整数k,n个点的连通图,可以被n/2条链路覆盖,且每条边至多被经过k次。

猜想三,n个点的连通图,可以被n/2条链路覆盖,且每条边恰好被经过1次。

3,相关定理

n个点的连通图,可以被n/2条链路或回路覆盖,且每条边恰好被经过1次。

4,应用

如芯片设计中的布线问题:如何把引脚连接起来,使得k最小,k就是布线通道。

四,回路覆盖问题

1,回路覆盖问题

寻找一组简单回路,使得任意边至少被经过1次。

显然存在覆盖方案等价于原图没有割边,所以我们只讨论无割边的图上的简单回路覆盖问题。

2,相关猜想

Itai-Rodeh猜想:n个点m条边的无割边图,存在一组简单回路覆盖,使得所有简单回路总边数不超过m+n-1。这个猜想已经被证明了。

Alon-Tarsi猜想:m条边的无割边图,存在一组简单回路覆盖,总边数不超过7m/5

PS:Alon-Tarsi猜想如果成立,那么2重回路覆盖猜想也是成立的。

3,最小权值回路覆盖

对于平面图,寻找一组简单回路,使得任意边至少被经过1次,且总权值最小。

可以在多项式时间内求出最小权值回路覆盖。

4,k重回路覆盖问题

寻找一组简单回路,使得任意边恰好被经过k次。

对于大于2的偶数,覆盖方案存在。对于k=2,这就是经典的2重回路覆盖问题。

5,2重回路覆盖问题

2重回路覆盖猜想:无割边图,存在一组简单回路覆盖,使得任意边恰好被经过2次。

如彼得森图的2重回路覆盖方案:

更多内容参考这里

6,1重回路覆盖问题

也叫圈分解问题:无割边图,是否存在一组简单回路覆盖,使得任意边恰好被经过1次。

欧拉定理:偶图存在圈分解。这是图论的第一个定理,因此把连通偶图称为欧拉图。

偶图即每个点的度数都是偶数的图,这种图显然没有割边。

7,欧拉图的最少圈覆盖问题

欧拉图一定存在圈覆盖,但是最少需要多少个圈呢?

Hajos猜想:n个点的欧拉图至多需要n/2个圈

Posa猜想:存在常数c,n个点的欧拉图至多需要cn个圈

五,中国邮递员问题

1,中国邮递员问题

求最短的回路,使得每条边至少经过1次。

对于无向图和有向图,这个问题有多项式级别的算法。

对于混向图,这是NP问题。

PS:中国邮递员问题不要求是简单回路,所以即使对于有割边的图也可能是有解的。

2,中国邮递员问题 VS 回路覆盖问题

回路覆盖问题的一个解,即一组简单回路,通过回路合并一定能得到一条回路。

但是中国邮递员问题的一个解,即一条回路,可能不是简单回路,所以可能无法转换成一组简单回路。

如,对于有割边的无向图,肯定是不存在一组简单回路可以覆盖所有的边,因为割边无法覆盖,但是对于中国邮递员问题来说是有解的。

六,子图覆盖问题

1,子图覆盖问题

给定一个图,寻找一组子图,使得每条边至少被1个子图覆盖。

实际上,链路覆盖问题和回路覆盖问题都是子图覆盖问题的一种特殊情况。

2,子图覆盖问题 VS 四色问题

四色问题等价于,每个无割边的平面图可以被2个偶子图覆盖,也等价于,每个无割边的平面图可以被3个偶子图覆盖,使得每条边恰好包含2次。

3,相关猜想

Fulkerson猜想:每个无割边的平面图可以被6个偶子图覆盖,使得每条边恰好包含4次。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值