状态压缩DP

状态压缩DP

  • 小国王
  • 玉米田
  • 炮兵阵地
  • 愤怒的小鸟
  • 宝藏
  • 蒙德里安的梦想
  • 最短Hamilton路径


小国王

在 n×n 的棋盘上放 k 个国王,国王可攻击相邻的 8 个格子,求使它们无法互相攻击的方案总数。

输入格式
共一行,包含两个整数 n 和 k。

输出格式
共一行,表示方案总数,若不能够放置则输出0。

数据范围
1 ≤ n ≤ 10,
0 ≤ k ≤ n2

输入样例:

3 2

输出样例:

16

算法解析
算法构造
这道题目,根据数据范围,不难得出,这道题目考察的是状态压缩动态规划。

分析题目,我们可以得到如下信息。

  1. 一个点的相邻八格,不可以有其他点。
  2. 棋盘置点类型。

那么,我们接下来,思考两个流程。

  1. 如何表示状态
  2. 如何转移方程

表示状态
显然,题目给的条件,是国王总数是严格限制的,就是k个。

所以说,我们放置了多少个国王,是需要考虑的。

接着,根据棋盘类型的状态压缩动态规划的套路,每一行的状态,我们需要明白。

也就是每一行,哪些位置放了国王。

综上所述,我们可以得出,动态规划的状态表示。

f[i][j][s]为所有只摆在前 i 行,目前摆了 j 个国王,而且第 i 行的摆放状态为 s

我们可以举一个例子

n=5
f[1][2][20]表示第一行,已经摆了两个国王,摆在左边第一个,和左边第三个
(20)10=(10100)2

状态转移
在这里,状态之间的转移,必然要满足,国王之间不会相互攻击到,那么我们进行分析。

两个国王,如果他们存在,直接靠近(上下左右)或者简介靠近(两斜对角),那么显然是不合法的。

因此,转换成为状态理解。

对于一个状态集合而言,显然不能存在相邻的1.

101(可以)两个国王有间隔
110(不可以)国王1和国王2相邻,可以相互攻击

因为这会导致,左右两个国王相邻,然后发起攻击。

而且,对于上下两行而言,不能有共同的一位有1

101
101

因为这会导致,上下两个国王相邻,然后发起攻击。

我们讨论完了,上下左右,接下来是最难的两斜对角。

我们设,第 i 行的状态为 a,第 i+1 行状态为 b

那么

S = a 或 b 也就是 S = a|b

是不可以存在,有相邻的1的。

a=100
b=010
S=110

因此这会导致,两斜对角国王相互攻击。

综上所述,我们得到集合转移的约束条件。

#include 
#include 
using namespace std;
typedef long long LL;
const int N = 12, M = 1 << 10, K = 110;
int n, m;
vector<int> state; // 存储合法状态 
int cnt[M]; // 存储每种状态1的数量 
vector<int> head[M]; // 没种状态的状态转移 
LL f[N][K][M]; // 所有只摆在前i行,目前摆了j个国王,而且第i行的摆放状态为s
bool check(int state)
{
	for (int i = 0; i < n; i ++ ) 
		if ((state >> i & 1) && (state >> i + 1 & 1))
			return false;
	return true;
} 
int count(int state)
{
	int res = 0;
	for (int i = 0; i < n; i ++ ) res += state >> i & 1;
	return res;
}
int main()
{
	cin >> n >> m;
	for (int i = 0; i < 1 << n; i ++ ) 	
		if (check(i)) { // 检查当前状态的合法性
			state.push_back(i);
			cnt[i] = count(i); 
		}
	for (int i = 0; i < state.size(); i ++ )
		for (int j = 0; j < state.size(); j ++ ) { 
			// 不同状态直接关系
			int a = state[i], b = state[j];
			if ((a & b) == 0 && check(a | b)) // 上下 斜对角 
				head[i].push_back(j); 
		}
	f[0][0][0] = 1;
	for (int i = 1; i <= n + 1; i ++ ) 
		for (int j = 0; j <= m; j ++ )
			for (int a = 0; a < state.size(); a ++ )
				for (int b : head[a]) {
					int c = cnt[state[a]];
					if (j >= c) 
						f[i][j][a] += f[i - 1][j - c][b]; 
				} 
	cout << f[n + 1][m][0];
	return 0;
}

笔记学习:
作者:秦淮岸灯火阑珊
链接:https://www.acwing.com/solution/content/10007/
来源:AcWing


玉米田

农夫约翰的土地由 M×N 个小方格组成,现在他要在土地里种植玉米。

非常遗憾,部分土地是不育的,无法种植。

而且,相邻的土地不能同时种植玉米,也就是说种植玉米的所有方格之间都不会有公共边缘。

现在给定土地的大小,请你求出共有多少种种植方法。

土地上什么都不种也算一种方法。

输入格式
第 1 行包含两个整数 M 和 N。

第 2…M+1 行:每行包含 N 个整数 0 或 1,用来描述整个土地的状况,1 表示该块土地肥沃,0 表示该块土地不育。

输出格式
输出总种植方法对 108 取模后的值。

数据范围
1 ≤ M,N ≤ 12

输入样例:

2 3
1 1 1
0 1 0

输出样例:

9

先求出一行中所有可能出现的状态,状态适用于每一行,然后遍历每一行、每一列,枚举全部状态可能性,根据题目判断全部合法的状态转移,然后存储下来。

算法解析

算法构造
经典的棋盘型状态压缩动态规划,我们可以按照之前小国王的思路,处理本题。

首先,我们需要明确,题目的要求:

  1. 统计方案数
  2. 有些土地不能种植

状态设计
首先,我们得明确状态是什么。

我们这个状态,肯定是要统计方案数。

我们这个状态,必然需要表示每一行土地种植的状态。

因此得到:

f[i][s]表示已经种植前i行,且第i行种植的状态为s的方案数

状态转移
题目的限制条件,其实就是我们转移的限制条件。

我们知道,这里是十字形的禁止种植,也就是上下左右不能有相邻的两棵玉米。

那么怎么判断呢?

如果说我们把1表示这个地方种植玉米,0表示不种植

S=11101,2,3这三个地方种玉米,第四个地方不种植玉米

对于一行而言,不能种植相邻的玉米。

即:

对于一行而言,不能有相邻的1

S=1110是不合法的状态

对于相邻的两行而言,不能在同一列都种植玉米

a=1010
b=1000
这是不可以的,在第一个位置会出现上下矛盾

那么我们可以转化为:

a&b==0

最后,对于题目中的土地不能种植,我们可以认为。

如果第i行的状态为s,那么荒废土地处不能有1

我们可以设计一个数组:

g[i]表示第i行不能种植土地的状态
g[1]=1011表示第一行,第一个,第三个,第四个位置不能种植玉米

总而言之

第i行的状态为s
那么s&g[i]==0
#include 
#include 
using namespace std;
const int N = 14, M = 1 << 12, mod = 1e8;
int n, m;
int w[N];
vector<int> state;
vector<int> head[M];
int f[N][M]; // 前i行 
bool check(int state)
{
	for (int i = 0; i + 1 < m; i ++ ) 
		if ((state >> i & 1) && (state >> i + 1 & 1))
			return false;
	return true;
}
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i ++ )
		for (int j = 0; j < m; j ++ ) {
			int t;
			cin >> t;
			w[i] += !t * (1 << j); // 存储农田状态,非运算方便后续判断状态 
		}
	for (int i = 0; i < 1 << m; i ++ ) // 一行中每一列的状态 
		if (check(i))
			state.push_back(i);
	for (int i = 0; i < state.size(); i ++ )
		for (int j = 0; j < state.size(); j ++ ) {
			int a = state[i], b = state[j];
			if (!(a & b)) // 相邻两行的合法状态转移 
				head[i].push_back(j);
		} 
	f[0][0] = 1;
	for (int i = 1; i <= n + 1; i ++ )
		for (int j = 0; j < state.size(); j ++ ) 
			if (!(state[j] & w[i])) // 当前状态针对当前行情况合法 
				for (int k : head[j])
					f[i][j] = (f[i][j] + f[i - 1][k]) % mod; // 状态转移 
	cout << f[n + 1][0]; // 始终多一行,状态不存数据,仅存储由之前状态转移来的总数据 
	return 0;
} 

笔记学习:
作者:秦淮岸灯火阑珊
链接:https://www.acwing.com/solution/content/17569/
来源:AcWing


炮兵阵地

司令部的将军们打算在 N×M 的网格地图上部署他们的炮兵部队。

一个 N×M 的地图由 N 行 M 列组成,地图的每一格可能是山地(用 H 表示),也可能是平原(用 P 表示),如下图。

在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示:
状态压缩DP_第1张图片
如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。

图上其它白色网格均攻击不到。

从图上可见炮兵的攻击范围不受地形的影响。

现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。

输入格式
第一行包含两个由空格分割开的正整数,分别表示 N 和 M;

接下来的 N 行,每一行含有连续的 M 个字符(P 或者 H),中间没有空格。按顺序表示地图中每一行的数据。

输出格式
仅一行,包含一个整数 K,表示最多能摆放的炮兵部队的数量。

数据范围
N ≤ 100,M ≤ 10

输入样例:

5 4
PHPP
PPHH
PPPP
PHPP
PHHP

输出样例:

6

状态压缩DP_第2张图片
状态压缩DP_第3张图片
因此在这两题里,我们只需压缩存储 当前层的状态 ,然后枚举 合法的上个阶段 的状态进行 转移 即可

但是本题的棋子攻击范围是 2,我们只压缩当前层一层状态后进行转移,是不能保证该次转移是 合法的

即不能排除第 i−2 层摆放的棋子可以攻击到第 i 层棋子的 不合法 情况

而解决该问题的手段就是:压缩存储两层的信息,然后枚举合法的第 i−2 层状态进行转移即可

a 为第 i 行的状态,用 for j 来枚举
b 为第i-1行的状态,用 for k 来枚举
c 为第i-2行的状态,用 for u 来枚举

f[i][j][k] 表示在前 i 行中摆放,第 i 行的状态为j,第i-1行的状态为k,的最大值
f[i-1][k][u]表示在前i-1行中摆放,第i-1行的状态为k,第i-2行的状态为u,的最大值
然后加上第i行摆放个数cnt[a]更新最大值,此题的最后一步的为第i-2行

#include 
#include 
using namespace std;
const int N = 10, M = 1 << 10;
int n, m;
int g[1010];
int f[2][M][M];
vector<int> state;
int cnt[M];
bool check(int state)
{
	for (int i = 0; i < m; i ++ ) 
		if ((state >> i & 1) && ((state >> i + 1 & 1) || (state >> i + 2 & 1)))
			return false;
	return true;	
} 
int count(int state)
{
	int res = 0;
	for (int i = 0; i < m; i ++ )
		if (state >> i & 1)
			res ++ ;
	return res;
}
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i ++ ) 
		for (int j = 0; j < m; j ++ ) {
			char c;
			cin >> c;
			g[i] += (c == 'H') << j; // 存储矩阵状态 
		}
	for (int i = 0; i < 1 << m; i ++ ) // 枚举一行可能的全部状态 
		if (check(i)) { // 判断合法 
			state.push_back(i); // 存储合法状态 
			cnt[i] = count(i); // 记录状态中的1 
		}
	// 枚举状态转移 
	for (int i = 1; i <= n; i ++ )
		for (int j = 0; j < state.size(); j ++ )
			for (int k = 0; k < state.size(); k ++ ) 
				for (int u = 0; u < state.size(); u ++ ) {
					int a = state[j], b = state[k], c = state[u];
					if (a & b | a & c | b & c) continue;
					if (g[i] & b | g[i - 1] & a) continue;
					// 滚动数组 
					// i&1 判断i是否为奇数
					// n为奇数时,对应的二进制数最低位一定为1,n&1的结果就是1
					// n为偶数时,相应的最低位为0,n&1的结果就是0 
					f[i & 1][j][k] = max(f[i & 1][j][k], f[i - 1 & 1][u][j] + cnt[b]);
				}
	int res = 0;
	for (int i = 0; i < state.size(); i ++ )
		for (int j = 0; j < state.size(); j ++ )
			res = max(res, f[n & 1][i][j]);
	cout << res;
	return 0;
} 

愤怒的小鸟

Kiana 最近沉迷于一款神奇的游戏无法自拔。

简单来说,这款游戏是在一个平面上进行的。

有一架弹弓位于 (0,0) 处,每次 Kiana 可以用它向第一象限发射一只红色的小鸟, 小鸟们的飞行轨迹均为形如 y=ax2+bx 的曲线,其中 a,b 是 Kiana 指定的参数,且必须满足 a<0。

当小鸟落回地面(即 x 轴)时,它就会瞬间消失。

在游戏的某个关卡里,平面的第一象限中有 n 只绿色的小猪,其中第 i 只小猪所在的坐标为 (xi,yi)。

如果某只小鸟的飞行轨迹经过了 (xi, yi),那么第 i 只小猪就会被消灭掉,同时小鸟将会沿着原先的轨迹继续飞行;

如果一只小鸟的飞行轨迹没有经过 (xi, yi),那么这只小鸟飞行的全过程就不会对第 i 只小猪产生任何影响。

例如,若两只小猪分别位于 (1,3) 和 (3,3),Kiana 可以选择发射一只飞行轨迹为 y=−x2+4x 的小鸟,这样两只小猪就会被这只小鸟一起消灭。

而这个游戏的目的,就是通过发射小鸟消灭所有的小猪。

这款神奇游戏的每个关卡对 Kiana 来说都很难,所以 Kiana 还输入了一些神秘的指令,使得自己能更轻松地完成这个这个游戏。

这些指令将在输入格式中详述。

假设这款游戏一共有 T 个关卡,现在 Kiana 想知道,对于每一个关卡,至少需要发射多少只小鸟才能消灭所有的小猪。

由于她不会算,所以希望由你告诉她。

注意:本题除 NOIP 原数据外,还包含加强数据。

输入格式
第一行包含一个正整数 T,表示游戏的关卡总数。

下面依次输入这 T 个关卡的信息。

每个关卡第一行包含两个非负整数 n,m,分别表示该关卡中的小猪数量和 Kiana 输入的神秘指令类型。

接下来的 n 行中,第 i 行包含两个正实数 (xi,yi),表示第 i 只小猪坐标为 (xi,yi),数据保证同一个关卡中不存在两只坐标完全相同的小猪。

如果 m=0,表示 Kiana 输入了一个没有任何作用的指令。

如果 m=1,则这个关卡将会满足:至多用 ⌈n/3+1⌉ 只小鸟即可消灭所有小猪。

如果 m=2,则这个关卡将会满足:一定存在一种最优解,其中有一只小鸟消灭了至少 ⌊n/3⌋ 只小猪。

保证 1 ≤ n ≤ 18,0 ≤ m ≤ 2,0 < xi,yi < 10,输入中的实数均保留到小数点后两位。

上文中,符号 ⌈c⌉ 和 ⌊c⌋ 分别表示对 c 向上取整和向下取整,例如 :⌈2.1⌉=⌈2.9⌉=⌈3.0⌉=⌊3.0⌋=⌊3.1⌋=⌊3.9⌋=3。

输出格式
对每个关卡依次输出一行答案。

输出的每一行包含一个正整数,表示相应的关卡中,消灭所有小猪最少需要的小鸟数量。

数据范围
状态压缩DP_第4张图片
输入样例:

2
2 0
1.00 3.00
3.00 3.00
5 2
1.00 5.00
2.00 8.00
3.00 9.00
4.00 8.00
5.00 5.00

输出样例:

1
1

状态压缩DP_第5张图片
算法
(状态压缩DP) O(T(n3+n2n))
一般抛物线方程:y=ax2+bx+c
题目中的抛物线有两个特点:

  1. 过原点, 即 c=0c=0
  2. 开口向下,即 a<0a<0

因此抛物线方程为:y=ax2+bx,有两个未知数,因此两点即可确定一条抛物线。

因此最多有 n2 个不同的抛物线。接下来求出所有不同的抛物线,及其能覆盖的所有点的点集。

此时问题变成了经典的“重复覆盖问题”,即给定01矩阵,要求选择尽量少的行,将所有列覆盖住。这里标准做法是使用 Dancing Links。
但由于 n<=18,因此可以直接使用状态压缩DP求解,代码更简单。

f[i] 表示当前已经覆盖的列是i时的最小行数。

转移时随便找到当前未被覆盖的某一列 x,然后枚举所有包含 x 的行j来选择即可。

即:f[i | j] = min(f[i | j], f[i] + 1)。

时间复杂度
预处理时需要枚举所有点对来确定抛物线,然后枚举其余点是否在抛物线上,计算量是 O(n3)。

状态压缩DP的过程中,一共有 2n 个状态,每个状态需要 O(n) 的计算量,因此每个Case的时间复杂度是 O(n3+n2n),总时间复杂度是 O(T(n3+n2n))。

#include 
#include 
#include 
#include 
#define x first
#define y second
using namespace std;
typedef pair<double, double> PDD;
const int N = 18, M = 1 << 18;
const double eps = 1e-8;
int n, m;
PDD q[N];
int path[N][N]; // 表示第i个点和第j个点构成的抛物线能覆盖的点的状态
int f[M];
int cmp(double x, double y)
{
	if (fabs(x - y) < eps) return 0;
	if (x < y) return -1;
	return 1;
}
int main()
{
	int T;
	cin >> T;
	while (T -- ) {
		cin >> n >> m;
		for (int i = 0; i < n; i ++ ) cin >> q[i].x >> q[i].y;
		memset(path, 0, sizeof path);
		for (int i = 0; i < n; i ++ ) {
			path[i][i] = 1 << i;
			for (int j = 0; j < n; j ++ ) {
				double x1 = q[i].x, y1 = q[i].y;
				double x2 = q[j].x, y2 = q[j].y;
				// 计算抛物线 
				if (!cmp(x1, x2)) continue;  // 抛物线上不存在斜率为无限大的切线
				double a = (y1 / x1 - y2 / x2) / (x1 - x2);
				double b = y1 / x1 - a * x1; 
				if (cmp(a, 0) >= 0) continue; // 此题抛物线需开口向下
				int state = 0;
				 // 计算path[i][j]的能覆盖的点的状态
				for (int k = 0; k < n; k ++ ) {
					double x = q[k].x, y = q[k].y;
					if (!cmp(a * x * x + b * x, y)) state += 1 << k; // 这一点在这条抛物线上
				}
				path[i][j] = state;
			}
		}
		memset(f, 0x3f, sizeof f);
		f[0] = 0;
		for (int i = 0; i + 1 < 1 << n; i ++ ) { // 遍历所有状态
			int x = 0;
			for (int j = 0; j < n; j ++ )
				if (!(i >> j & 1)) {
					x = j;
					break;
				}
			for (int j = 0; j < n; j ++ )
				f[i | path[x][j]] = min(f[i | path[x][j]], f[i] + 1);
		}
		cout << f[(1 << n) - 1] << endl;
	}
	return 0;
}

笔记、代码学习:
作者:yxc
链接:https://www.acwing.com/solution/content/4028/
来源:AcWing


宝藏


蒙德里安的梦想

求把 N×M 的棋盘分割成若干个 1×2 的的长方形,有多少种方案。

例如当 N=2,M=4 时,共有 5 种方案。当 N=2,M=3 时,共有 3 种方案。

如下图所示:
在这里插入图片描述
输入格式
输入包含多组测试用例。

每组测试用例占一行,包含两个整数 N 和 M。

当输入用例 N=0,M=0 时,表示输入终止,且该用例无需处理。

输出格式
每个测试用例输出一个结果,每个结果占一行。

数据范围
1 ≤ N,M ≤ 11

输入样例:

1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0

输出样例:

1
0
1
2
3
5
144
51205

状态压缩DP_第6张图片
状态压缩DP_第7张图片
核心:
摆放的小方格方案数 等价于 横着摆放的小方格方案数

如何判断当前方案是否合法?
遍历每一列,i列的方案数只和i-1列有关系

  • j&k==0, i-2列伸到i-1的小方格 和i-1列放置的小方格 不重复。
  • 每一列,所有连续着空着的小方格必须是偶数个

dp分析:
状态表示 f[i][j]: 前i-1列已经确定,且从第i-1列伸出的小方格在第i列的状态为j 的方案数。
属性:个数。

i=2, j=11001 表示下列图的状态
状态压缩DP_第8张图片
但是i-1, i列已经固定,所以集合划分是依据i-2 列伸到 i-1 列的不同状态 k 来划分

i-2 列伸到 i-1 列的状态 k=00100
状态压缩DP_第9张图片
状态压缩DP_第10张图片
状态计算:(限制条件:i-1列非空白位置可以不能放置小方格),在i列不同的放置方法就是不同的集合划分。

问题:第 i-2 列伸到 i-1 列的状态为 k , 是否能成功转移到 第 i-1 列伸到 i 列的状态为 j ?

需要满足如下条件:

  • j&k==0, i-2列伸到i-1的小方格 和i-1列放置的小方格 不重复。
  • 每一列,所有连续着空着的小方格必须是偶数个

f[m][0]:
列数从0开始计数,m列不放小方格,前m-1列已经完全摆放好并且不伸出来的状态

#include 
#include 
using namespace std;
const int N = 12, M = 1 << N;
int n, m;
long long f[N][M]; // 第i列 状态是j 
bool st[M];
int main()
{
	while (cin >> n >> m, n || m) { 
		for (int i = 0; i < 1 << n; i ++ ) { // 预处理:枚举一列的全部状态,找合法状态 
			int cnt = 0; // 记录一列中连续0的个数 
			st[i] = true;
			for (int j = 0; j < n; j ++ ) // 枚举该状态的每一位 
				if (i >> j & 1) { // 当前位为1 
					if (cnt & 1) st[i] = false; // 如果之前连续的0的个数为奇数,则当前状态不可取 
					cnt = 0;
				} 
				else cnt ++ ; // 当前位为0 
			if (cnt & 1) st[i] = false; // 如果当前有奇数个连续的0则状态不可取 
		}
		memset(f, 0, sizeof f);
		f[0][0] = 1;
		for (int i = 1; i <= m; i ++ ) // 每一列 
			for (int j = 0; j < 1 << n; j ++ ) // 枚举列的状态 
				for (int k = 0; k < 1 << n; k ++ ) // 前一列的状态 
					if ((j & k) == 0 && (st[j | k])) // 如果它们没有冲突,i这一列被占位的情况也是合法的话
						f[i][j] += f[i - 1][k]; // 那么这种状态下它的方案数等于之前每种k状态数目的和
		cout << f[m][0] << endl; // 求的是第m-1行排满,并且第m-1行不向外伸出块的情况 0~m-1行是题目中可以摆方块的范围
	}
	return 0;
}

最短Hamilton路径

给定一张 n 个点的带权无向图,点从 0∼n−1 标号,求起点 0 到终点 n−1 的最短 Hamilton 路径。

Hamilton 路径的定义是从 0 到 n−1 不重不漏地经过每个点恰好一次。
51
输入格式
第一行输入整数 n。

接下来 n 行每行 n 个整数,其中第 i 行第 j 个整数表示点 i 到 j 的距离(记为 a[i,j])。

对于任意的 x,y,z,数据保证 a[x,x]=0,a[x,y]=a[y,x] 并且 a[x,y]+a[y,z]≥a[x,z]。

输出格式
输出一个整数,表示最短蒟蒻 Hamilton 路径的长度。

数据范围
1 ≤ n ≤ 20
0 ≤ a[i,j] ≤ 107

输入样例:

5
0 2 4 5 1
2 0 6 5 3
4 6 0 8 3
5 5 8 0 5
1 3 3 5 0

输出样例:

18

首先我们要思考如果让这个NP完全题目复杂度降低,那么可以优先考虑到使用位运算,状态压缩等解决思路。

状态压缩DP分析:
1.本题思路
假设:一共有七个点,用0,1,2,3,4,5,6来表示,那么先假设终点就是5,在这里我们再假设还没有走到5这个点,且走到的终点是4,那么有以下六种情况:
first: 0–>1–>2–>3–>4 距离:21
second: 0–>1–>3–>2–>4 距离:23
third: 0–>2–>1–>3–>4 距离:17
fourth: 0–>2–>3–>1–>4 距离:20
fifth: 0–>3–>1–>2–>4 距离:15
sixth: 0–>3–>2–>1–>4 距离:18

如果此时你是一个商人你会走怎样的路径?显而易见,会走第五种情况对吧?因为每段路程的终点都是4,且每种方案的可供选择的点是0~4,而商人寻求的是走到5这个点的最短距离,而4到5的走法只有一种,所以我们选择第五种方案,可寻找到走到5这个点儿之前,且终点是4的方案的最短距离,此时0 ~ 5的最短距离为(15+4走到5的距离).(假设4–>5=8)

同理:假设还没有走到5这个点儿,且走到的终点是3,那么有一下六种情况:
first: 0–>1–>2–>4–>3 距离:27
second: 0–>1–>4–>2–>3 距离:22
third: 0–>2–>1–>4–>3 距离:19
fourth: 0–>2–>4–>1–>3 距离:24
fifth: 0–>4–>1–>2–>3 距离:26
sixth: 0–>4–>2–>1–>3 距离:17

此时我们可以果断的做出决定:走第六种方案!!!,而此时0~5的最短距离为(17+3走到5的距离)(假设3–>5=5)

在以上两大类情况之后我们可以得出当走到5时:

  1. 以4为终点的情况的最短距离是:15+8=23;
  2. 以3为终点的情况的最短距离是:17+5=22;

经过深思熟虑之后,商人决定走以3为终点的最短距离,此时更新最短距离为:22。

当然以此类推还会有以1为终点和以2为终点的情况,此时我们可以进行以上操作不断更新到5这个点的最短距离,最终可以得到走到5这个点儿的最短距离,然后再返回最初的假设,再依次假设1,2,3,4是终点,最后再不断更新,最终可以得出我们想要的答案。

接着思考,我们可以发现,我们所需要的不是整个方案,而只是方案最优解,所以我们只需要记录当前这个方案的最优解即可,那么我们考虑的状态,不就只有,在当前方案 i 中,目前抵达的点是 j。

2.DP分析:
用二进制来表示要走的所以情况的路径,这里用i来代替
例如走0,1,2,4这三个点,则表示为:10111;
走0,2,3这三个点:1101;

状态表示:f[i][j];
集合:所有从0走到j,走过的所有点的情况是i的所有路径
属性:MIN
状态计算:如1中分析一致,0–>·····–>k–>j中k的所有情况
状态压缩DP_第11张图片
状态转移方程:f[i][j]=min(f[i][j],f[i^(1<

以上转移方程,weight数组为权值 ,也就是weight[k][j]是k点到 j 点的权值

i^(1<

那么这个位运算有什么用处呢,第一点它是在判断第j位的情况,第二点位运算处理速度很快。

#include 
#include 
using namespace std;
const int N = 20, M = 1 << N;
int n;
int w[N][N];
int f[M][N]; // 状态是i 最后一个字母是j 的最短路径 
int main()
{
	cin >> n;
	for (int i = 0; i < n; i ++ )
		for (int j = 0; j < n; j ++ ) 
			cin >> w[i][j];
	memset(f, 0x3f, sizeof f);
	f[0][0] = 0;
	for (int i = 0; i < 1 << n; i ++ ) // 枚举全部状态 
		for (int j = 0; j < n; j ++ ) // 枚举全部终点 
			if (i >> j & 1) // 状态可达终点
				for (int k = 0; k < n; k ++ ) // 上一个点
					if (i >> k & 1)
						f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]); 
	cout << f[(1 << n) - 1][n - 1];
	return 0;
} 

笔记学习:
作者:灰之魔女
链接:https://www.acwing.com/solution/content/18533/
来源:AcWing
作者:秦淮岸灯火阑珊
链接:https://www.acwing.com/solution/content/789/
来源:AcWing


你可能感兴趣的:(AcWing算法提高课,动态规划,算法)