插头 DP

2023-10-09 07:04
文章标签 dp 插头

本文主要是介绍插头 DP,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

垃圾插头DP,照着打都调了我一下午,淦!!!

学这个玩意纯粹是因为模拟赛考了一道,要不然碰都不会碰……

我觉得插头DP的主要难度在于实现,而不是理解算法原理……

不说废话了,进入正题。

解决的问题

学一个新知识点,肯定先要了解这个知识点能够解决什么样的问题。(废话)

其实插头DP就是一类特殊的状压DP,他解决的就是在小数据规模下连通性的问题,数据太大插头DP也不好做,毕竟本质还是状压。

举几个简单的例子,就比如说格点图的哈密顿路径计数,求棋盘的黑白染色方案满足相同颜色之间形成一个连通块的方案数,以及特定图的生成树计数等等,这些你看起来连暴力都不是很好搞的东西,插头DP就可以做。(说实话,就这复杂度,说暴力都有人信

接下来我们先来了解几个概念。

基本概念


插头

如果一个格子某个方向有插头,表示这个格子在这个方向与相邻格子是相连的。

简单点来说就是记录当前格子与相邻的格子是否连通。

轮廓线

就是已决策格子与未决策格子的分界线。(下图红色的线)

在这里插入图片描述

注意一下,这里分界线的边数是列数加一,别搞错了。


大概就这两个概念,我们现在来看看插头DP如何实现

实现

我们就借一道板题来说。

Luogu P5056 【模板】插头DP

因为数据较小,但是搜索过不了,于是考虑状压。(即插头DP)

可以逐格DP(某些题目可以逐行),设 f i , j , s t a t e f_{i,j,state} fi,j,state 表示当前的方案数。

通常的 s t a t e state state 有括号表示和最小表示,这里着重介绍泛用性更好的最小表示。我们用长度为 m + 1 m+1 m+1 的整形数组,记录轮廓线上每个插头的状态, 0 0 0 表示没有插头,并约定连通的插头用相同的数字进行标记。

由于要形成最小回路,可知最多只有两个插头互相连通,并且跟两组连通的插头之间不会交叉,既不会出现 2 1 2 1 2\ 1\ 2\ 1 2 1 2 1 的情况,并且每一个格子最多只有两个插头。

考虑如何转移。

在这里插入图片描述

对于上图黄色的格子,轮廓线为红色, 蓝色使我们DP完当前节点后的轮廓线(包括第一、二、四列的红线),我们分以下几种情况考虑。

  • 跟上面和左边的格子都有插头,此时还要分两种情况。
    • 如果这两个插头没连通,则这个格子则不能有右插头和下插头,即新的两边轮廓线上没有任何插头。
    • 如果这两个插头连通了,若当前枚举的格子为最后一个格子,则直接连通,否则不能连通。(若连通则成为多个闭合回路了)
  • 跟上面和左边的格子有一个插头,则此时跟下面和右边任意一个格子也有一个插头。
  • 上面和左边都没有插头,则此时右边和下面都必须有插头。

注意考虑一下边界和障碍的条件。

当我们DP完一行时,轮廓线会变为下图形式:

在这里插入图片描述
此时我们在DP下一行之前记得将整个状态左移即可。

优化

对于普通的插头DP,时空复杂度也并不是非常优,此时就需要一点优化。

我们会发现由于他是闭合回路,它的状态会非常稀疏,于是我们可以用 hash 来优化。

但这个 hash 要用挂表法,即将你所有模数为 x x x 的状态都挂到一起,可以用链式前向星维护。

你每次遇到一个新状态就将它存到哈希里面,然后查找状态则将那一条 hash 链枚举一遍即可。

代码

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 200005, P = 9973, offset = 4, mask = (1 << offset) - 1;
int n, m, mp[20][20], ex, ey;
LL Val;
int in() {char ch = getchar();while (ch != '.' && ch != '*')ch = getchar();return ch == '*';
}
struct HashTable {//hash的实现int last[P], next[N], cnt;LL ans[N], state[N];void clear() {cnt = 0;memset(last, 0, sizeof(last));}void push(LL s) {LL x = s % P;for (int i = last[x]; i; i = next[i])if (state[i] == s) {ans[i] += Val;return ;}cnt++, state[cnt] = s, ans[cnt] = Val;next[cnt] = last[x], last[x] = cnt;}void roll() {for (int i = 1; i <= cnt; i++)state[i] = state[i] << offset;}
} H[2], *H0, *H1;
int bb[25], b[25];
LL encode() {//这是将数组加码成状态LL s = 0;memset(bb, -1, sizeof(bb));int bn = 1;bb[0] = 0;for (int i = m + 1; i; i--) {if (!~bb[b[i]])bb[b[i]] = bn++;s <<= offset;s |= bb[b[i]];}return s;
}
void decode(LL s) {//这是将状态解码成数组for (int i = 1; i <= m + 1; i++) {b[i] = s & mask;s >>= offset;}
}
void push(int j, int down, int right) {//将状态改变,然后往下DPb[j] = down, b[j + 1] = right;H1 -> push(encode());
}
void PlugDP() {//DPH0 = H, H1 = H + 1;H1 -> clear(), Val = 1, H1 -> push(0);for (int i = 1; i <= n; i++) {for (int j = 1; j <= m; j++) {swap(H0, H1), H1 -> clear();for (int l = 1; l <= (H0 -> cnt); l++) {decode(H0 -> state[l]);Val = H0 -> ans[l];int left = b[j], up = b[j + 1];//上面和左边的插头类型bool down = i != n, right = j != m;;//这两个变量即是判断是否处于边界位置,能否有右插头和下插头if (mp[i][j] == 1) {//如果当前格子为障碍if (!left && !up) {//如果上面和左边都没有插头,则可以转移成下面和右边也都没有插头push(j, 0, 0);}}else if (left && up) {//上面和左边都有插头if (left == up) {//两个插头连通if (i == ex && j == ey)push(j, 0, 0);}else {//两个插头不连通for (int t = 1; t <= m + 1; t++)if (b[t] == left)b[t] = up;push(j, 0, 0);}}else if (left || up) {//上面和左边只有一个插头int t = left | up;if (down)push(j, t, 0);if (right)push(j, 0, t);}else if (down && right)//两边都没有插头并且下面和右边都可以有插头push(j, m, m);}}H1 -> roll();//即全部右移}
}
int main() {scanf("%d%d", &n, &m);for (int i = 1; i <= n; i++)for (int j = 1; j <= m; j++) {mp[i][j] = in();if (!mp[i][j])ex = i, ey = j;}PlugDP();cout << (H1 -> cnt == 1 ? H1 -> ans[1] : 0) << endl;return 0;
}

这篇关于插头 DP的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/171167

相关文章

hdu4826(三维DP)

这是一个百度之星的资格赛第四题 题目链接:http://acm.hdu.edu.cn/contests/contest_showproblem.php?pid=1004&cid=500 题意:从左上角的点到右上角的点,每个点只能走一遍,走的方向有三个:向上,向下,向右,求最大值。 咋一看像搜索题,先暴搜,TLE,然后剪枝,还是TLE.然后我就改方法,用DP来做,这题和普通dp相比,多个个向上

hdu1011(背包树形DP)

没有完全理解这题, m个人,攻打一个map,map的入口是1,在攻打某个结点之前要先攻打其他一个结点 dp[i][j]表示m个人攻打以第i个结点为根节点的子树得到的最优解 状态转移dp[i][ j ] = max(dp[i][j], dp[i][k]+dp[t][j-k]),其中t是i结点的子节点 代码如下: #include<iostream>#include<algorithm

hdu4865(概率DP)

题意:已知前一天和今天的天气概率,某天的天气概率和叶子的潮湿程度的概率,n天叶子的湿度,求n天最有可能的天气情况。 思路:概率DP,dp[i][j]表示第i天天气为j的概率,状态转移如下:dp[i][j] = max(dp[i][j, dp[i-1][k]*table2[k][j]*table1[j][col] )  代码如下: #include <stdio.h>#include

usaco 1.1 Broken Necklace(DP)

直接上代码 接触的第一道dp ps.大概的思路就是 先从左往右用一个数组在每个点记下蓝或黑的个数 再从右到左算一遍 最后取出最大的即可 核心语句在于: 如果 str[i] = 'r'  ,   rl[i]=rl[i-1]+1, bl[i]=0 如果 str[i] = 'b' ,  bl[i]=bl[i-1]+1, rl[i]=0 如果 str[i] = 'w',  bl[i]=b

uva 10154 DP 叠乌龟

题意: 给你几只乌龟,每只乌龟有自身的重量和力量。 每只乌龟的力量可以承受自身体重和在其上的几只乌龟的体重和内。 问最多能叠放几只乌龟。 解析: 先将乌龟按力量从小到大排列。 然后dp的时候从前往后叠,状态转移方程: dp[i][j] = dp[i - 1][j];if (dp[i - 1][j - 1] != inf && dp[i - 1][j - 1] <= t[i]

uva 10118 dP

题意: 给4列篮子,每次从某一列开始无放回拿蜡烛放入篮子里,并且篮子最多只能放5支蜡烛,数字代表蜡烛的颜色。 当拿出当前颜色的蜡烛在篮子里存在时,猪脚可以把蜡烛带回家。 问最多拿多少只蜡烛。 代码: #include <iostream>#include <cstdio>#include <cstdlib>#include <algorithm>#include <cs

uva 10069 DP + 大数加法

代码: #include <iostream>#include <cstdio>#include <cstdlib>#include <algorithm>#include <cstring>#include <cmath>#include <stack>#include <vector>#include <queue>#include <map>#include <cl

uva 10029 HASH + DP

题意: 给一个字典,里面有好多单词。单词可以由增加、删除、变换,变成另一个单词,问能变换的最长单词长度。 解析: HASH+dp 代码: #include <iostream>#include <cstdio>#include <cstdlib>#include <algorithm>#include <cstring>#include <cmath>#inc

XTU 1233 n个硬币连续m个正面个数(dp)

题面: Coins Problem Description: Duoxida buys a bottle of MaiDong from a vending machine and the machine give her n coins back. She places them in a line randomly showing head face or tail face o

dp算法练习题【8】

不同二叉搜索树 96. 不同的二叉搜索树 给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。 示例 1: 输入:n = 3输出:5 示例 2: 输入:n = 1输出:1 class Solution {public int numTrees(int n) {int[] dp = new int