首页 > 其他分享 >插头 dp

插头 dp

时间:2025-01-20 09:55:24浏览次数:1  
标签:插头 状态 联通 int 端点 dp

1 引入

插头 dp 是一种基于连通性状态压缩的动态规划,这一类问题要求我们记录元素的联通状况,例如在棋盘上走出一条回路等。此时朴素的状压 dp 难以处理,所以需要引入插头 dp 帮助我们求解。

开始前需要了解两个基本概念:

  • 轮廓线:已决策状态和未决策状态的分界线。
  • 插头:对于四连通的网格来讲,每一个格子有上下左右四个插头,一个插头存在表示这个格子在这个方向与相邻格子相连。

2 多条回路问题

在了解上述基本概念之后,实际上我们就可以直接来看一下多条回路的问题了。尽管实际操作中并没有利用连通性维护信息,但是有助于我们先一步了解插头 dp 的基本转移模式。

例题:P5074 Eat the Trees

2.1 状态转移

首先轮廓线的长度一定是 \(m+1\),包含 \(m\) 个横向分界和 \(1\) 个纵向分界。对于这 \(m+1\) 条分界线,我们只需要维护它们上面有无插头即可。令 \(dp(i,j,S)\) 表示考虑完格子 \((i,j)\),轮廓线状态为 \(S\) 的方案数。不难发现,与当前格子有关的两个插头分别位于第 \(j,j+1\) 位上,记其值分别为 \(x,y\)。然后进行分类讨论:

  • 若 \(a_{i,j}=0\),则这里不能有插头。判断之后转移即可。
  • 若 \(x=y=0\),则当前格子没有上插头和左插头,为了使这个格子被覆盖,它必然要有下插头和右插头。
  • 若 \(x=1,y=0\),则当前格子只有左插头,那么它可以有右插头或者有下插头。
  • 若 \(x=0,y=1\),则当前格子只有上插头,那么它可以有右插头或者有下插头。
  • 若 \(x=y=1\),则当前格子有上插头和左插头,此时它们已经构成了路径,所以它不会再有下插头或右插头了。

接下来考虑行间转移,显然结束状态是纵向分界在最右侧边界上,于是要保证它没有插头。删掉这个分界后加入下一行最开头的分界线即可。最后答案为 \(f(n,m,0)\)。

2.2 代码

#include <bits/stdc++.h>
#define il inline
#define int long long

using namespace std;

const int Maxn = 2e5 + 5;
const int Inf = 2e9;

int T;
int n, m, a[15][15];
int dp[15][15][1 << 12];

void solve() {
	cin >> n >> m;
	for(int i = 1; i <= n; i++) {
		for(int j = 1; j <= m; j++) {
			cin >> a[i][j];
		}
	}
	int M = (1 << (m + 1)) - 1;
	for(int i = 1; i <= n; i++) {
		for(int j = 0; j <= m; j++) {
			for(int k = 0; k <= M; k++) dp[i][j][k] = 0;
		}
	}
	dp[1][0][0] = 1;
	for(int i = 1; i <= n; i++) {
		for(int j = 1; j <= m; j++) {
			for(int k = 0; k <= M; k++) {
				int x = (k >> (j - 1)) & 1, y = (k >> j) & 1;
				if(!a[i][j]) {
					if(!x && !y) dp[i][j][k] += dp[i][j - 1][k];
				}
				else {
					if(x + y == 1) {
                        //可以通过位运算简化分类讨论
						dp[i][j][k ^ (3 << (j - 1))] += dp[i][j - 1][k];
						dp[i][j][k] += dp[i][j - 1][k];
					}
					else {
						dp[i][j][k ^ (3 << (j - 1))] += dp[i][j - 1][k];
					}
				}
			}
		}
		for(int k = 0; k < (1 << m); k++) {
			dp[i + 1][0][k << 1] += dp[i][m][k];//向下一行转移
		}
	}
	cout << dp[n][m][0] << '\n';
}

signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> T;
	while(T--) solve();
	return 0;
}

实际中还可以通过滚动数组优化掉前两维,此处不做赘述。

3 一条回路问题

上面的题目并没有涉及插头 dp 的维护连通性这个要点,接下来我们来看真正的模板题。

例题:【模板】插头 DP

由于此时我们需要区分插头之间的连通性,所以仅仅记录轮廓线上是否存在插头已经不够用了。所以我们需要新的表示方法来记录轮廓线状态,然后再进行转移。

3.1 状态编码

3.1.1 最小表示法

最小表示法可以用于表示插头构成的联通块信息。具体来讲,我们对每一个联通块标号,每一个联通块的标号就是当前已有联通块个数加一,并对所有属于该联通块的点均标记为该编号。

例如对于某个轮廓线,其上插头所属联通块序列为 \((2,1,2,1,3,2,3)\),我们的最小表示为 \((1,2,1,2,3,1,3)\)。值得注意的是如果有插头没有所属联通块记为 \(0\)。

3.1.2 括号表示法

括号表示法可以表示插头构成的回路或路径信息。对于回路而言,轮廓线上的部分一定构成了若干个互不相交的路径。那么我们就可以得出,对于任意联通块,恰有两个插头属于这个联通块(因为路径的两个端点都是插头);并且由于路径互不相交,所以如果我们将每一条路径对应的两个插头视作一个区间,那么所有路径构成的区间之间只可能相离或包含,不可能相交。

根据上面两个性质,我们不难想到维护方式了——括号序列。对于一个联通块的两个端点,分别将其看作左括号和右括号,分别记为 \(1,2\),那么合法状态一定对应一个合法的括号序列。对于没有插头的点依然看作 \(0\)。

注意只有回路或路径才有上面的性质,可以使用括号表示法;而联通块没有上述性质,只能采用最小表示法。

3.2 状态转移

此题的状态转移与 2.1 中的转移讨论方式基本一致,但是我们需要额外讨论 \(x,y\) 具体的值以满足连通性要求。此处我们采用括号表示法来描述状态。

这里我们重新强调,原先的插头状态是 \(x,y\),假如我们转移后该节点下插头和右插头状态为 \(p,q\),那么实际上每次转移的时候就是将 \(x,y\) 改成 \(p,q\)。

  • \((i,j)\) 不允许铺线,那么只有 \(x=y=0\) 时可以转移,此时 \(p=q=0\)。

  • \(x=y=0\) 时,为了满足经过每一个点的要求,必须新建两个插头。于是 \(p=1,q=2\)。

  • \(x\ne 0,y=0\) 时,只能有下插头或右插头,并且这个插头应该作为原先 \(x\) 状态的延伸,所以 \(p=x,q=0\) 或者 \(p=0,q=x\)。

  • \(x=0,y\ne 0\) 时,同上。

  • \(x\ne 0,y\ne 0\) 时,我们只能合并这两个插头,所以 \(p=q=0\)。但是此时我们还要根据 \(x,y\) 具体的值做分类讨论。

    • \(x=1,y=2\) 时,这两个插头合并会形成回路。对于一条回路的题目来讲,这种转移只能在 \((n,m)\) 处进行,标志着形成了回路。

    • \(x=2,y=1\) 时,两个联通块被合并成了一个联通块,除了改动 \(x,y\) 以外没有改动。

    • \(x=1,y=1\) 时,由于我们将两个联通块的左端点合并在了一起,那么原先 \(y\) 对应的右端点会变成新联通块的左端点,所以我们需要找到 \(y\) 对应的右端点,将其修改为一个左端点。

      至于如何找到这个位置,直接暴力向右枚举并记录左端点和右端点个数,相等时即找到匹配端点。

    • \(x=2,y=2\) 时,同上。

于是我们按此进行转移即可。

3.3 状态存储

采用括号表示法的好处在于它的状态好表示,发现所有状态只有 \(0,1,2\) 三种,所以可以用三进制数表示。不过实际中我们多采用四进制数来表示,这样的好处是我们可以用二进制数来表示(两位算一个数),然后就可以用位运算了。

但是发现采用四进制后我们的状态总数会来到 \(2^{24}\),直接枚举显然不可行。考虑到括号序列的特殊性,我们并不会有很多的合法状态,所以考虑利用哈希表来存储所有 dp 值。然后你就发现你必须要写滚动数组了:)

3.4 代码

#include <bits/stdc++.h>
#define il inline
#define int long long

using namespace std;

const int Maxn = 2e5 + 5;
const int Inf = 2e9;
const int P = 9973;

int n, m;
int a[15][15];
int ex, ey;

struct HashTable {
	int head[P], nxt[Maxn], tot;
	int st[Maxn], dp[Maxn];
	il void clear() {
		tot = 0;
		for(int i = 0; i < P; i++) head[i] = 0;
	}
	il void ins(int sta, int val) {
		int id = sta % P;
		for(int i = head[id]; i; i = nxt[i]) {
			if(st[i] == sta) {
				dp[i] += val;
				return ;
			}
		}
		nxt[++tot] = head[id], st[tot] = sta, dp[tot] = val;
		head[id] = tot;
	}
}f[2];

il int getw(int sta, int i) {//找出状态中第 i 位
	return (sta >> ((i - 1) << 1)) & 3;
}
il int setw(int sta, int i, int w) {//将状态中第 i 位改为 w
	return sta ^ (getw(sta, i) << ((i - 1) << 1)) ^ (w << ((i - 1) << 1));
}
il int getr(int sta, int i) {//找到匹配右端点
	int res = 0;
	for(int p = i; p <= m + 1; p++) {
		int ret = getw(sta, p);
		if(ret == 1) res++;
		else if(ret == 2) res--;
		if(!res) return p;
	}
	return -1;
}
il int getl(int sta, int i) {//找到匹配左端点
	int res = 0;
	for(int p = i; p >= 1; p--) {
		int ret = getw(sta, p);
		if(ret == 2) res++;
		else if(ret == 1) res--;
		if(!res) return p;
	}
	return -1;
}

signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m;
	for(int i = 1; i <= n; i++) {
		string s; cin >> s;
		for(int j = 1; j <= m; j++) {
			a[i][j] = (s[j - 1] == '.' ? 1 : 0);
			if(a[i][j]) ex = i, ey = j;
		}
	}
	int pre = 0, now = 1;
	f[pre].ins(0, 1);
	int ans = 0;
	for(int i = 1; i <= n; i++) {
		for(int j = 1; j <= m; j++) {
			for(int k = 1; k <= f[pre].tot; k++) {
				int sta = f[pre].st[k], val = f[pre].dp[k];
				int x = getw(sta, j), y = getw(sta, j + 1);
				if(!a[i][j]) {
					if(!x && !y) f[now].ins(sta, val);
				}
				else {
					if(!x && !y) {
						int nst = setw(setw(sta, j, 1), j + 1, 2);
						f[now].ins(nst, val);
					}
					else if(!x || !y) {
						f[now].ins(sta, val);
						int nst = setw(setw(sta, j, y), j + 1, x);
						f[now].ins(nst, val);
					}
					else {
						int nst = setw(setw(sta, j, 0), j + 1, 0);
						if(x == 1 && y == 2) {
							if(i == ex && j == ey && nst == 0) ans += val;
                            //注意统计答案的时候要在最后一个可以经过的格子统计
						}
						else if(x == 2 && y == 1) {
							f[now].ins(nst, val);
						}
						else if(x == 1 && y == 1) {
							int pos = getr(sta, j + 1);
							nst = setw(nst, pos, 1);
							f[now].ins(nst, val);
						}
						else if(x == 2 && y == 2) {
							int pos = getl(sta, j);
							nst = setw(nst, pos, 2);
							f[now].ins(nst, val);
						}
					}
				}
			}		
			swap(pre, now);
			f[now].clear();		
		}
		for(int k = 1; k <= f[pre].tot; k++) {
			int sta = f[pre].st[k], val = f[pre].dp[k];
			if(getw(sta, m + 1) == 0) f[now].ins(sta << 2, val);
		}
		swap(pre, now);
		f[now].clear();
	}
	cout << ans << '\n';
	return 0;
}

4 例题

插头 dp 的关键点在于设计出插头状态,并且分类讨论出所有可能的转移。下面举几例说明如何设计插头状态和转移。

例 1 [SCOI2011] 地板

\(\text{Link}\)

这道题要求的覆盖条件必须是 L 型。考虑到一个 L 型意味着必须要有转折,于是设计三个插头状态:

  • \(0\):无插头。
  • \(1\):当前插头还没有转折。
  • \(2\):当前插头已经转折。

接下来分类讨论转移即可。转移并不困难,留给读者自行完成。

例 2 [CQOI2015] 标识设计

\(\text{Link}\)

发现此题与上一题很像,不过此时的 L 型图案不能旋转,所以转移的时候要注意对方向的讨论。

但是这道题的重点不在这里,我们发现此题中 \(n,m\le 30\),如果暴力存储所有状态而不加剪枝的话是会爆炸的。所以我们需要利用当前新建的 L 型个数来进行判断(注意不是已经形成的个数),当其数量等于 \(3\) 的时候就不能再新建一个 L 型了。

如此可以大大减少枚举量,可以通过。

例 3 [BZOJ2310] ParkII

题意: 从方格中选出一条路径,使路径上数字之和最大。

此时我们选的不是回路,而是路径,因此必然会有两个单独的端点。并且会发现与端点联通的插头是不会和任何另外的插头构成联通块的。所以除了回路中设计的 \(0,1,2\) 三个插头状态之外,我们还需要一个插头状态 \(3\),用于表示该插头与路径端点联通。

然后分类讨论数量就从 \(3\times 3\) 变成了 \(4\times 4\) 种,所以讨论的时候一定要足够细致。另一个问题在于由于是一条路径,所以端点个数只能有两个,因此必须要记录当前的端点个数然后再转移。

例 4 [WC2008] 游览计划

\(\text{Link}\)

这道题就不是路径或者回路,而是联通块了,所以只能用最小表示法。

关于最小表示法有这样一件事:由于一行最多 \(10\) 个格子,所以一行上最多会有 \(5\) 个不同的联通块,所以我们只需要用八进制来存储最小表示法的状态即可。并且由于此时求的是联通块,所以把轮廓线放在网格线上其实是浪费了一个位置的(就是纵向分界的那一条边,显然它一定等于前一个横向分界),于是可以将轮廓线放到网格上处理。不过我还是更习惯放到网格线上处理。

然后就是转移了,我们依然对 \(x,y\) 做分类讨论。这里着重说明新建联通块和合并联通块的过程。

  • 当 \(x=y=0\) 时,我们可能会新建一个联通块。此时我们直接将这两个位置赋成 \(7\),表示它们位于一个联通块,也不会和其他编号重复。接下来我们需要实现一个函数,对当前的序列重构最小表示,然后重构出的序列就是当前状态了。
  • 当 \(x\ne 0,y\ne 0,x\ne y\) 时,我们可能会将两个联通块合并起来。这个时候我们遍历当前序列,将所有的 \(x\) 改成 \(y\),就可以将两者合并。然后再跑一边上面的重构函数就可以得到当前状态的最小表示了。

剩下的部分就比较简单了,留给读者自行完成。和例 3 一样的,我们也需要进行剪枝,如果当前状态中联通块数量小于记录的联通块数量且后者不为 \(1\),那么说明上面一定有一个不和其他联通块联通的联通块,此时就不该转移。

由于本题还需要输出最优方案,所以我们还需要另开一个哈希表存储当前状态的最优解是从上一个位置的哪个状态转移而来,以及最优解情况下这个点需不需要放,然后反推一遍就可以得到最优方案了。

标签:插头,状态,联通,int,端点,dp
From: https://www.cnblogs.com/UKE-Automation/p/18680816

相关文章

  • 基础DP 做题记录
    LuoguP1192台阶问题Link简要题意:给定台阶数\(n\le1e5\)和一步至多跨越台阶数\(k\le1e2\),初始在\(0\)级,求方案数\(\pmod{1e5+3}\)。思路:设\(f_i\)表示走到第\(i\)级台阶的方案数,题意直接说明了可以从前\(k\)级台阶转移过来,考虑每次在以经处理好的台阶前新加一级......
  • MPLS LDP原理与配置
    一.简介MPLS,称之为多协议标签交换,在九十年代中期被提出来,用于解决传统IP报文依赖查表转发而产生的瓶颈,现多用于VPN技术,MPLS报头封装在数据链路层之上,网络层之下。本文为结合了华为技术和新华三技术的大成,即结合了HCIA,HCIP,HCIEDatacom和H3CNE-RS+,H3CSE-RS+,H3CIE-RS+。本文将主......
  • 插入dp学习笔记
    定义插入\(\text{dp}\)适用于计数、求最优解且具有选择、排列元素过程等题目。插入\(\text{dp}\)大致分为两类:乱搞型:状态定义天马行空,但始终围绕着将新元素插入到旧元素已有集合中套路型:\(dp_{i,j}\)表示前\(i\)个数,现在构成\(j\)个连续段的方案数\(/\)最优解,此外......
  • 如何解决WordPress打开网页时出现“建立数据库连接时出错”的问题?
    常见原因数据库配置文件错误:wp-config.php文件中的数据库配置信息不正确。MySQL数据库服务问题:MySQL数据库服务未启动或数据库账号密码错误。网络连接问题:如果使用外部数据库,可能需要检查网络连接和端口配置。解决方法方法一:检查数据库配置文件打开wp-config.php文件:......
  • 解决WordPress上传至虚拟主机后网站无法正常显示的问题
    用户将WordPress程序上传至虚拟主机空间中,配置了wp-config.php文件并上传,但网站无法正常显示,提示错误信息。回答: 您好,感谢您的反馈。根据您的描述,可能是由于数据库配置不正确导致的。具体来说,数据库配置连接虚拟主机数据库没有数据,访问会跳转到安装目录,原因是未导入数据库备份文......
  • Chromium CDP 开发(十三):为自己的Domain建立CustomCDPHandler
    引言在开发ChromiumCDP(ChromeDevToolsProtocol)时,除了创建PDL和JSON文件来定义自定义的CDPDomain和指令外,还需要编写对应的Handler实现文件,以便使这些自定义指令能够被正确执行。本章将详细介绍如何为自定义的CDPDomain创建和实现Handler文件custom_cdp_ha......
  • wordpress插件本地测试
    1.本地搭建wordpress,以便测试wordpress网站安装的插件,避免在服务器上的插件不合适,卸载之后,会有冗余数据存在数据库中,影响网站性能。在本地搭建wordpress,使用的是USBWebserver,以下是具体操作流程:建站软件:USBWebserver快速搭建本地PHP环境.md·知月来/wordpress-码云-开源......
  • 动态规划(dp数组)
    动态规划,是利用历史记录来避免重复计算的一种算法,是求解决策过程最优化的过程。一般用一维数组/二维数组来保存历史记录。(将原问题拆解成若干子问题,同时保存子问题的答案,使每个子问题只求解一次,最终获得原问题的答案。)一般动态规划有三个步骤:1.定义数组元素的含义,一般求什么就......
  • Profibus DP转Modbus TCP协议转换网关模块功能详解
    ProfibusDP和ModbusTCP是两种不同的工业现场总线协议,ProfibusDP常用于制造业自动化领域,而ModbusTCP则在工业自动化和楼宇自动化等领域广泛应用。实现ProfibusDP转ModbusTCP功能,通常需要特定的网关设备,以下为你详细介绍:捷米JM-DPM-TCP网关模块这......
  • DP 蓝题思想精选
    P1099[NOIP2007提高组]树网的核如果有多条直径,对于任意一个直径的分差点,其在单调队列里的贡献就是另半条直径。而对于其其他分支,如果贡献大于那半条直径,那么这个贡献就会成为新的直径,与题设不符。因此,讨论任意一条直径即可。P1136迎接仪式容易注意到,一个点可以与前面、后面......