哈密顿回路、链路、其他点覆盖问题

目录

一,哈密顿回路、哈密顿链路

二,相关定理

1,Dirac定理

2,Tutte定理

3,竞赛图必有哈密顿链路

三,应用

1,旅行商问题(TSP)

2,芯片通孔

四,求解哈密顿链路

力扣 996. 正方形数组的数目

力扣 980. 不同路径 III

五,相关puzzle

1,数字满格、数阵迷踪、数字迷途、数字连线

2,踏马、立方骑士

3,矩形图中马的哈密顿回路、链路

六,点的回路覆盖问题

1,回路覆盖问题

2,相关定理

3,应用

七,最短哈密顿链路

1,模板

2,力扣 943. 最短超级串


一,哈密顿回路、哈密顿链路

图的哈密顿回路是指包含图的所有节点的回路。

图的哈密顿链路是指包含图的所有节点的链路。

二,相关定理

1,Dirac定理

如果G是一个n(n≥3)个点的无向简单图,所有节点的度数都 ≥ n/2.0,则G中存在哈密顿回路。

证明过程:

(1)G是连通图

(2)设最长路径是L,用抽屉原理可以证明L是个环

(3)如果有节点不在环中,根据连通性可以加入一个点把环变成一条更长的路径。所以所有节点都在这个环中。

2,Tutte定理

所有4连通平面图都有哈密顿圈。证明过程没研究。

关于平面图,参考常见的图

3,竞赛图必有哈密顿链路

 关于竞赛图,参考常见的图

竞赛图中不存在环当且仅当所有顶点的出度从小到大排列依次为0, 1, 2, … , n-1 。

翻译成赛事语言就是,单循环赛结果没有环,当且仅当所有人的实力是线性排序的,没有任何一次比赛发生逆袭现象。

三,应用

1,旅行商问题(TSP)

给定一系列城市和每对城市之间的距离,求解访问每一座城市一次并回到起始城市的最短回路。

这是NP难问题。

2,芯片通孔

芯片上有很多不同尺寸的通孔,用于走线。

同一尺寸的通孔用1个钻头全部打完之后,再换钻头。

对于1个钻头来说,如何走最短的距离把所有通孔打完?

四,求解哈密顿链路

哈密顿问题其实和TSP问题差不多,是NP问题,没有特别好的算法。

回溯法:

class Hamilton
{
public:
	stack<int> hami;//哈密顿链路
	Hamilton(int n, map<int, vector<int>>& m, int type)//type=0是无向图 1是有向图
	{
		this->n = n;
		this->m = m;
		this->type = type;
		for (int i = 0; i < n; i++)dfs(i);
	}
private:
	bool dfs(int k)
	{
		s.push(k);
		if (s.size() == n) {
			hami = s;
			return true;
		}
		for (auto nk : m[k]) {
			if (visit[k])continue;
			visit[k] = 1;
			if (dfs(nk))return true;
			visit[k] = 0;
		}
		s.pop();
		return false;
	}
	int n;
	int type;
	map<int, vector<int>> m;//邻接表
	map<int, int>visit;
	stack<int>s;
};

力扣 996. 正方形数组的数目

给定一个非负整数数组 A,如果该数组每对相邻元素之和是一个完全平方数,则称这一数组为正方形数组。

返回 A 的正方形排列的数目。两个排列 A1 和 A2 不同的充要条件是存在某个索引 i,使得 A1[i] != A2[i]。

示例 1:

输入:[1,17,8]
输出:2
解释:
[1,8,17] 和 [17,8,1] 都是有效的排列。

示例 2:

输入:[2,2,2]
输出:1

提示:

  1. 1 <= A.length <= 12
  2. 0 <= A[i] <= 1e9
vector<int> gnums;
class Hamilton
{
public:
	map<long long, int>ans;
	Hamilton(int n, map<int, vector<int>>& m, int type)//type=0是无向图 1是有向图
	{
		this->n = n;
		this->m = m;
		this->type = type;
		for (int i = 0; i < n; i++)dfs(i,1);
	}
private:
	bool dfs(int k,int deep)
	{
		if (visit[k])return false;
		long long h2 = h;
		hash(k);
		if (hashVisit[h])
			goto RET;
		hashVisit[h] = 1;
		if (deep == n) {
			ans[h] = 1;
			h = h2;
			return true;
		}
		visit[k] = 1;
		for (auto nk : m[k]) {
			dfs(nk,deep+1);
		}
		RET:
		visit[k] = 0;
		h = h2;
		return false;
	}
	void hash(int k)
	{
		h = ((h * (gnums[k]+123) + 666) * 789)%1000000007;
	}
	int n;
	int type;
	map<int, vector<int>> m;//邻接表
	map<int, int>visit;
	map<long long, int>hashVisit;
	long long h = 1234567;
};

class Solution {
public:
	int numSquarefulPerms(vector<int>& nums) {
		int n = nums.size();
		map<int, vector<int>>m;
		for (int i = 0; i < n; i++)for (int j = i + 1; j < n; j++) {
			if (isSquare(nums[i] + nums[j]))m[i].push_back(j), m[j].push_back(i);
		}
		gnums = nums;
		return Hamilton(n, m, 0).ans.size();
	}
	bool isSquare(int n) {
		int x = sqrt(n);
		return x * x == n;
	}
};

力扣 980. 不同路径 III

在二维网格 grid 上,有 4 种类型的方格:

  • 1 表示起始方格。且只有一个起始方格。
  • 2 表示结束方格,且只有一个结束方格。
  • 0 表示我们可以走过的空方格。
  • -1 表示我们无法跨越的障碍。

返回在四个方向(上、下、左、右)上行走时,从起始方格到结束方格的不同路径的数目

每一个无障碍方格都要通过一次,但是一条路径中不能重复通过同一个方格

示例 1:

输入:[[1,0,0,0],[0,0,0,0],[0,0,2,-1]]
输出:2
解释:我们有以下两条路径:
1. (0,0),(0,1),(0,2),(0,3),(1,3),(1,2),(1,1),(1,0),(2,0),(2,1),(2,2)
2. (0,0),(1,0),(2,0),(2,1),(1,1),(0,1),(0,2),(0,3),(1,3),(1,2),(2,2)

示例 2:

输入:[[1,0,0,0],[0,0,0,0],[0,0,0,2]]
输出:4
解释:我们有以下四条路径: 
1. (0,0),(0,1),(0,2),(0,3),(1,3),(1,2),(1,1),(1,0),(2,0),(2,1),(2,2),(2,3)
2. (0,0),(0,1),(1,1),(1,0),(2,0),(2,1),(2,2),(1,2),(0,2),(0,3),(1,3),(2,3)
3. (0,0),(1,0),(2,0),(2,1),(2,2),(1,2),(1,1),(0,1),(0,2),(0,3),(1,3),(2,3)
4. (0,0),(1,0),(2,0),(2,1),(1,1),(0,1),(0,2),(0,3),(1,3),(1,2),(2,2),(2,3)

示例 3:

输入:[[0,1],[2,0]]
输出:0
解释:
没有一条路能完全穿过每一个空的方格一次。
请注意,起始和结束方格可以位于网格中的任意位置。

提示:

  • 1 <= grid.length * grid[0].length <= 20
class Hamilton
{
public:
	map<long long, int>ans;
	Hamilton(int n, map<int, vector<int>>& m, int type,int start,int e)//type=0是无向图 1是有向图
	{
		this->n = n;
		this->m = m;
		this->type = type;
		this->e = e;
		dfs(start,1);
	}
private:
	bool dfs(int k,int deep)
	{
		if (visit[k])return false;
		long long h2 = h;
		hash(k);
		if (hashVisit[h])
			goto RET;
		hashVisit[h] = 1;
		if (deep == n) {
			ans[h] = 1;
			h = h2;
			return true;
		}
		visit[k] = 1;
		for (auto nk : m[k]) {
			if (deep < n - 1 && nk == e)continue;
			dfs(nk,deep+1);
		}
		RET:
		visit[k] = 0;
		h = h2;
		return false;
	}
	void hash(int k)
	{
		h = ((h * (k+123) + 666) * 789)%1000000007;
	}
	int n;
	int type;
	map<int, vector<int>> m;//邻接表
	map<int, int>visit;
	map<long long, int>hashVisit;
	long long h = 1234567;
	int e;
};


class Solution {
public:
	int uniquePathsIII(vector<vector<int>>& grid) {
		row=grid.size(),col = grid[0].size();
		int n = row*col;
		map<int, vector<int>>m;
		int s, e;
		for (int i = 0; i < row; i++)for (int j = 0; j < col; j++) {
			if (grid[i][j] == -1) {
				n--;
				continue;
			}
			if (grid[i][j] == 1)s = id(i, j);
			if (grid[i][j] == 2)e = id(i, j);
			vector<int> v = getNeighbor4(id(i, j));
			for (int k : v)if(grid[k/col][k%col]!=-1)m[k].push_back(id(i, j));
		}
		return Hamilton(n, m, 0,s,e).ans.size();
	}
	int id(int x, int y)
	{
		return x * col + y;
	}
	vector<int> getNeighbor4(int k)
	{
		vector<int>ans;
		if (k >= col)ans.push_back(k - col);
		if (k < (row - 1) * col)ans.push_back(k + col);
		if (k % col)ans.push_back(k - 1);
		if (k % col < col - 1)ans.push_back(k + 1);
		return ans;
	}
	int row,col;
};

五,相关puzzle

1,数字满格、数阵迷踪、数字迷途、数字连线

数字满格、数阵迷踪、数字迷途、数字连线

2,踏马、立方骑士

踏马、立方骑士

3,矩形图中马的哈密顿回路、链路

定理:

如果n是偶数(n>4)那么n*n的棋盘有一个哈密顿回路。

如果n是奇数(n>3)那么n*n的棋盘有一个哈密顿链路。

像这样的图还可以构造一些,不过基本上都差不多。

总结起来就是,最中间的格子是比较特殊的,上图中标号1-8的这8个格子构成1个回路,

其余的16个格子也构成1个回路,相当于5*5的棋盘其实就是3个回路组合而成,只要适当地选择断点,即可把3个回路连接成一条链。

如果选取的起点不同,得到的哈密顿链还是略有区别的。

比如:112象棋(12)

关于上面的2个定理,书上的意思是用数学归纳法来证明,从n=k变成n=k+4相当于在原来的基础上,套上了一个宽度为2的边框。这个边框是若干个哈密顿回路组成的,只要在适当的地方断开,就可以和里面的k*k连接起来。

下面讨论,7*7的棋盘去掉中间的3*3如何形成哈密顿回路。

首先,角落的点只有2个邻居,那么在回路里面,这2个点也一定都是他的邻居。

其他的,以此类推,可以得到下图:

对于那些已经连了2条线的点,自然是没有任何悬念了,所以接下来只需要考虑那些恰好连了1条线的点该如何连接起来。将上图化简如下:

黑色的线表示一定相连,红色的线表示可能相连。

这样,可以得到这16个点的哈密顿回路:

与此对应,可以得到原图:

这是由2个哈密顿回路构成的。

这样,整个通过把大的回路断开一个地方,可以得到下图:

在把紫色的哈密顿回路和这个长为41的哈密顿链连接起来,即可得到7*7的哈密顿链

六,点的回路覆盖问题

1,回路覆盖问题

对于没有割边的图,求一组简单回路,覆盖所有的点,且总边数最少。

2,相关定理

n个点的无割边图,存在一组简单回路,覆盖所有的点,且总边数不超过2n-2

3,应用

如果按照这个方式建设网络,就能得到边数很少的无割边图。

无割边意味着至少是2连通,比普通的连通图稳定性更好。

七,最短哈密顿链路

对于有向图,求所有的哈密顿链路中,长度最短的那条。

1,模板

思路:状态压缩DP

时间复杂度:O(n^2 * 2^n)

//最短哈密顿链路
template<typename T = int>
class MinHamilton
{
public:
	MinHamilton(int n, DirectedGraphData<T>& g) :g(g) //n的上限大概是15
	{
		this->n = n;
		startId = g.startId;
		m.clear();
		path.clear();
	}
	vector<int> getMinPath()
	{
		int allId = (1 << n) - 1;
		int ans = INT_MAX;
		for (int i = 0; i < n; i++) {
			ans = min(ans, dp(i, allId ^ (1 << i)));
		}
		for (int i = 0; i < n; i++) {
			if (m[i][allId ^ (1 << i)] == ans) {
				reBuild(i, allId ^ (1 << i), ans);
				return path;
			}
		}
		return vector<int>{};
	}
private:
	int dp(int id, int allOtherId)
	{
		auto& mi = m[id];
		if (mi.find(allOtherId) != mi.end())return mi[allOtherId];
		if (!allOtherId)return mi[allOtherId] = 0;
		int ans = INT_MAX;
		for (int i = 0; i < n; i++) {
			if ((allOtherId & (1 << i)) == 0)continue;
			auto it = g.edgeMap.find(make_pair(id, startId + i));
			if (it == g.edgeMap.end())continue;
			int x = dp(i, allOtherId ^ (1 << i));
			if (x < INT_MAX)ans = min(ans, x + it->second);
		}
		return mi[allOtherId] = ans;
	}
	void reBuild(int id, int allOtherId, int ans)
	{
		path.push_back(id);
		if (!allOtherId)return;
		for (int i = 0; i < n; i++) {
			if ((allOtherId & (1 << i)) == 0)continue;
			auto it = g.edgeMap.find(make_pair(id, startId + i));
			if (it == g.edgeMap.end())continue;
			if (m[i][allOtherId ^ (1 << i)] == ans - it->second) {
				return reBuild(i, allOtherId ^ (1 << i), ans - it->second);
			}
		}
		return vector<int>{};
	}
private:
	DirectedGraphData<T>& g;
	int n;
	int startId;
	map<int, map<int, int>>m;
	vector<int>path;
};

2,力扣 943. 最短超级串

给定一个字符串数组 words,找到以 words 中每个字符串作为子字符串的最短字符串。如果有多个有效最短字符串满足题目条件,返回其中 任意一个 即可。

我们可以假设 words 中没有字符串是 words 中另一个字符串的子字符串。

示例 1:

输入:words = ["alex","loves","leetcode"]
输出:"alexlovesleetcode"
解释:"alex","loves","leetcode" 的所有排列都会被接受。

示例 2:

输入:words = ["catg","ctaagt","gcta","ttca","atgcatc"]
输出:"gctaagttcatgcatc"

提示:

  • 1 <= words.length <= 12
  • 1 <= words[i].length <= 20
  • words[i] 由小写英文字母组成
  • words 中的所有字符串 互不相同

class Solution {
public:
	string shortestSuperstring(vector<string>& words) {
		if (words.size() == 1)return words[0];
		vector<DirectedEdge<>> edges;
		for (int i = 0; i < words.size(); i++) {
			for (int j = 0; j < words.size(); j++) {
				if (i == j)continue;
				edges.push_back(DirectedEdge<>{vector<int>{i, j, -sameNum(words[i], words[j])}});
			}
		}
		DirectedGraphData<> g(edges);
		MinHamilton<> opt(words.size(), g);
		auto v = opt.getMinPath();
		string ans = words[v[0]];
		for (int i = 1; i < v.size(); i++) {
			int len = sameNum(words[v[i - 1]], words[v[i]]);
			ans += words[v[i]].substr(len, words[v[i]].length() - len);
		}
		return ans;
	}
	int sameNum(string& s1, string& s2) {
		int len = min(s1.length(), s2.length());
		for (int i = len - 1; i; i--) {
			if (s1.substr(s1.length() - i, i) == s2.substr(0, i))return i;
		}
		return 0;
	}
};

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值