【学习笔记】简单的连通性状压DP——插头DP(不学以为是天书)

文章目录

  • 哈希链表
  • 插头DP
    • 概念
    • 括号表示法 / 最小表示法
    • 例题
      • 洛谷插头dp板题
      • CITY
      • ParkII
      • Tony's Tour
      • Efficient Tree
      • [CQOI2015]标识设计

哈希链表

众所周知,哈希是有冲突的可能性的,而且在状态数越多,冲突的概率就越高。目前掌握的处理方案有多哈希,但仍有冲突的可能;STL\text{STL}STL 直接整个记录下来,自带大常数和 log\text{log}log
插头 DP 都不用,而是采取了哈希链表的方法。

具体而言就是将轮廓线上插头状态按一定的规则哈希成一个整数后,甩到其对应的链上。

形象地说,就是开了一定的哈希桶,然后哈希后的整数取模桶数,得到一个桶的编号,接着把这个轮廓线相应信息装成结构体,放到这个桶里。显然这个桶里很有可能有其余的轮廓线状态,那么就直接接在最后一个的后面。用前向星,数组来做。应该不会有人要刚vector吧

桶数随意,但肯定是越大且是个质数更好,因为后面定位轮廓线哈希位置的时候,是要从其所在桶里面开始一个一个遍历比较的,如果一个桶内太多轮廓线状态,遍历时间复杂度就有点要命了。

img

#define mod 299989
struct HashTable { int sta[2], dp[2], nxt; }Hash[300000];
void insert( int sta, int val ) {int key = sta % mod;for( int i = head[key];i;i = Hash[i].nxt )if( sta == Hash[i].sta[now] ) { Hash[i].dp[now] += val; return; }++ cnt[now];Hash[cnt[now]].sta[now] = sta;Hash[cnt[now]].dp[now] = val;Hash[cnt[now]].nxt = head[key];head[key] = cnt[now];
};

插头DP

概念

插头DP ,是一类基于连通性的状态压缩动态规划,用状压DP来处理联通问题。本质就是状压。

常见的联通问题:多回路问题、路径问题、简单回路问题、广义路径问题、生成树问题。

插头DP 一般是 逐格转移 的,少有逐行转移。

而逐格转移就是将格子划分成已转移的格子和未转移的格子,我们将起到这样分类作用的工具叫做 轮廓线

如图,红色线就是此时的轮廓线,当前在处理的格子是黄色格子,也就是说当前处理的格子也被划分到未转移的格子集合中。

不难发现,mmm 列却有 m+1m+1m+1 个插头 / 轮廓线。

在这里插入图片描述

既然叫做 插头DP,那么什么又是插头呢?插头就是一个格子上下左右四个方向的插头,如图。

前面提到了主要是用来解决连通性问题的,一个格子与其余格子联通,怎么联通的

在这里插入图片描述

这个插头可以帮助我们知道两个相邻格子是怎样连接的,比如这个格子是和上面格子联通的,那么就是一条上下边,是通过这个格子的上插头完成的。还有如果这个格子恰好是联通的拐点,就是通过相邻插头,比如从左插头联通这个格子又从下插头连出去。

一个格子有四个插头,一个存在的插头表示 在它代表的方向上 能与相邻的格子连接(联通)。不存在就不能。

要求一个回路,也就意味着最后所有的非障碍格子通过插头连接成了一个连通块,那么久需要转移时记录格子的连通情况。

我们递推的时候就是依据轮廓线上插头的存在性,求出所有能转移到的合法状态。

形象地理解,每个格子可以看作一块块拼图,插头就是两块拼图之间的衔接。

在逐格转移中一般只记录轮廓线上的插头存在性,因为这些才与后面的格子是有可能直接相连的。

在这里插入图片描述

转移到新格子后,轮廓线也会相应的移动。如图。不难发现,同行内的移动只跟当前处理格子的左插头和上插头有关,并且是转移到自己的右插头和下插头。如果换行,发现唯一一个竖着的特殊插头是不会有的。但不影响,后面会提到。
所以当前格子合法状态至于上一次处理的格子有关,这里就可以采用滚动数组优化了,减少空间花费。

在这里插入图片描述

一般是设 dp[i][j][sta]:(i,j)dp[i][j][sta]:(i,j)dp[i][j][sta]:(i,j) 位置时状态是 stastasta 的方案数 / 代价和 等等。状压的就是轮廓线状态。

括号表示法 / 最小表示法

记录当前的联通状态 / 轮廓线上的插头状态,准确地讲是记录轮廓线上的状态,一般有两种方法。

括号表示法

当前已经联通的轮廓线上的插头,靠左边的是左括号,靠右边的是右括号。没有插头就另设字符。

轮廓线上从左到右 a,b,c,da,b,c,da,b,c,d 插头,如果 a,ca,ca,c 连通,并且与 bbb 不连通,那么 b,db,db,d 一定不连通。这个性质对所有的棋盘模型的问题都适用。

感性理解这很显然。

从左到右的排序是从左边的第一条轮廓线走到右边第一条轮廓线依次经历轮廓线的顺序。

最小表示法

同样地,从左到右的顺序,找到第一个不是障碍的轮廓线插头,然后找到跟这个插头联通的其余轮廓线插头,标记为第一个连通块,然后找第二个不是障碍且没有标记的插头,以此类推。

在这里插入图片描述

最小表示法比较好写,但括号表示法似乎时间更优秀。括号表示法是要每种情况分类讨论的,回路到还好只有三种状态,只用讨论九种,要是简单路径就会涉及到单独插头的问题,就是讨论十六种了。写挂的概率直线飙升。

最小表示法是有些情况可以合并成一个写。

且括号表示法的状态修改较为麻烦,都不一样,所以无法合并情况,而最小表示法比较暴力直接解码重编。

竖着的那条轮廓线比较特殊,所以我一般把它放到第 000 位。

例题

由于涉及到哈希压缩,一般都是用位运算,所以有些题目虽然是列的上限是 555 这种,我们也习惯开成 888 位的,这在最小表示法中很常见。

当你题目做多了过后,就会发现基本上都是一样的框架的感觉。

洛谷插头dp板题

luogu P5056 【模板】插头dp

此题就用括号表示法来做吧。

  • 新建一个连通块

    这个新建是就当前已操作过的格子而言的,但本题肯定是最后会联通成一个的,否则就不合法了。

    能新建联通块当且仅当这个格子的上边轮廓线和左边轮廓线都没有插头。如图。

    在这里插入图片描述

  • 合并两个联通块

    那么肯定是两个轮廓线都指了插头,这里必须做合并操作,不然就无法形成回路。

    • 合并的是两个不同连通块的右括号,那么就要找两个右括号相对应的左括号更靠右的一个,变成右括号。

    在这里插入图片描述

    • 合并的是两个不同连通块的左括号,那么就要找两个左括号相对应的右括号更靠左的一个,变成左括号。

    在这里插入图片描述

    • 合并的是两个联通块的左右括号,直接擦去即可。

      在这里插入图片描述最后一个格子肯定是也是合并的这个类型,形成了完整的一条回路,这个时候还需要判断是否轮廓上一个插头都不存在,是否合法,然后统计答案。
      在这里插入图片描述

  • 延续之前的联通情况。

    • 不拐弯。

      不拐弯最简单,不用改轮廓线情况,直接插入就行。

      注意虽然下面这张图的括号表示法看似发生了改变,但是前面我提过,我喜欢把竖着个特殊轮廓线放在第 000 位,所以是没有改变的。

      具体可见代码。

      有的人就喜欢在 (i,j)(i,j)(i,j) 时,jjj 代表格子横着的轮廓线,j−1j-1j1 代表特殊的竖轮廓线。那可能就需要修改??

      在这里插入图片描述

      在这里插入图片描述

    • 拐弯的。

      拐弯的其实也简单,就是直接延续。虽然括号表示法看起来没有变化,但实际上是有轮廓线改变的。

      具体可见代码。

      在这里插入图片描述
      在这里插入图片描述

#include <bits/stdc++.h>
using namespace std;
#define mod 298999
#define int long long
int n, m;
int lst, now;//滚动
int head[300000], cnt[2]; //滚动的总合法方案数
bool mp[15][15];
//0 1 2 没有插头 左括号 右括号
struct HashTable { int sta[2], dp[2], nxt; }Hash[300000];
void insert( int sta, int val ) {int key = sta % mod;for( int i = head[key];i;i = Hash[i].nxt )if( sta == Hash[i].sta[now] ) { Hash[i].dp[now] += val; return; }++ cnt[now];Hash[cnt[now]].sta[now] = sta;Hash[cnt[now]].dp[now] = val;Hash[cnt[now]].nxt = head[key];head[key] = cnt[now];
};struct node { int s[15]; };
node unZip( int sta ) { //解压node code;code.s[0] = sta & 3; //单独的竖直轮廓线for( int i = 1;i <= m;i ++ ) code.s[i] = ( sta >> ( i << 1 ) ) & 3;return code;
}
int Zip( node code ) { //压缩int sta = 0;for( int i = 1;i <= m;i ++ ) sta |= ( code.s[i] << ( i << 1 ) );sta |= code.s[0];return sta;
}signed main() {int ans = 0, Endx, Endy;scanf( "%lld %lld", &n, &m ); char ch[20];for( int i = 1;i <= n;i ++ ) {scanf( "%s", ch + 1 );for( int j = 1;j <= m;j ++ )if( ch[j] == '.' ) mp[i][j] = 1, Endx = i, Endy = j;}insert( 0, 1 );for( int i = 1;i <= n;i ++ ) {for( int j = 1;j <= m;j ++ ) {lst = now, now ^= 1, cnt[now] = 0;memset( head, 0, sizeof( head ) );for( int k = 1;k <= cnt[lst];k ++ ) {node code = unZip( Hash[k].sta[lst] );int Left = code.s[0], Up = code.s[j]; //左插头 上插头int dp = Hash[k].dp[lst];if( ! mp[i][j] ) {if( ! Left and ! Up ) insert( Zip( code ), dp );}else if( ! Left and ! Up ) {if( mp[i + 1][j] and mp[i][j + 1] ) {code.s[0] = 2, code.s[j] = 1;insert( Zip( code ), dp );}}else if( ! Left and Up ) {if( mp[i + 1][j] ) insert( Zip( code ), dp );if( mp[i][j + 1] ) {code.s[0] = Up, code.s[j] = 0;insert( Zip( code ), dp );}}else if( Left and ! Up ) {if( mp[i][j + 1] ) insert( Zip( code ), dp );if( mp[i + 1][j] ) {code.s[0] = 0, code.s[j] = Left;insert( Zip( code ), dp );}}else if( Left == 1 and Up == 1 ) {//不属于同一个连通块 都是左括号//得连起来 然后与这两个左括号匹配的右括号中较近的一个改成左括号 方法使用括号匹配//显然肯定是左插头的匹配括号在右 呈包含关系int p, tot = 1;for( p = j + 1;p <= m;p ++ ) {if( code.s[p] == 1 ) tot ++;if( code.s[p] == 2 ) tot --;if( ! tot ) break;}code.s[0] = code.s[j] = 0, code.s[p] = 1;insert( Zip( code ), dp );}else if( Left == 2 and Up == 2 ) {//显然肯定是上插头的匹配括号在左 呈包含关系int p, tot = -1;for( p = j - 1;p;p -- ) {if( code.s[p] == 1 ) tot ++;if( code.s[p] == 2 ) tot --;if( ! tot ) break;}code.s[0] = code.s[j] = 0, code.s[p] = 2;insert( Zip( code ), dp );}else if( Left == 2 and Up == 1 ) {code.s[0] = code.s[j] = 0;insert( Zip( code ), dp );}else if( Left == 1 and Up == 2 ) { //回路形成code.s[0] = code.s[j] = 0; bool flag = 0;//判断是否合法for( int p = 0;p <= m;p ++ )if( code.s[i] ) { flag = 1; break; }if( ! flag and i == Endx and j == Endy ) ans += dp;}}}}printf( "%lld\n", ans );return 0;
}

CITY

BZOJ3125

如果板题看懂了,就会发现这道题只是有些限制需要判定而已。直接改改就能过。

#include <bits/stdc++.h>
using namespace std;
#define mod 298999
#define int long long
int n, m;
int lst, now;//滚动
int head[300000], cnt[2]; //滚动的总合法方案数
int mp[15][15];
//0 1 2 没有插头 左括号 右括号
struct HashTable { int sta[2], dp[2], nxt; }Hash[300000];
void insert( int sta, int val ) {int key = sta % mod;for( int i = head[key];i;i = Hash[i].nxt )if( sta == Hash[i].sta[now] ) { Hash[i].dp[now] += val; return; }++ cnt[now];Hash[cnt[now]].sta[now] = sta;Hash[cnt[now]].dp[now] = val;Hash[cnt[now]].nxt = head[key];head[key] = cnt[now];
};struct node { int s[15]; };
node unZip( int sta ) { //解压node code;code.s[0] = sta & 3; //单独的竖直轮廓线for( int i = 1;i <= m;i ++ ) code.s[i] = ( sta >> ( i << 1 ) ) & 3;return code;
}
int Zip( node code ) { //压缩int sta = 0;for( int i = 1;i <= m;i ++ ) sta |= ( code.s[i] << ( i << 1 ) );sta |= code.s[0];return sta;
}signed main() {int ans = 0, Endx, Endy;scanf( "%lld %lld", &n, &m ); char ch[20];for( int i = 1;i <= n;i ++ ) {scanf( "%s", ch + 1 );for( int j = 1;j <= m;j ++ ) {if( ch[j] == '#' ) continue;if( ch[j] == '.' ) mp[i][j] = 1;if( ch[j] == '-' ) mp[i][j] = 2;if( ch[j] == '|' ) mp[i][j] = 3;Endx = i, Endy = j;}}insert( 0, 1 );for( int i = 1;i <= n;i ++ ) {for( int j = 1;j <= m;j ++ ) {lst = now, now ^= 1, cnt[now] = 0;memset( head, 0, sizeof( head ) );for( int k = 1;k <= cnt[lst];k ++ ) {node code = unZip( Hash[k].sta[lst] );int Left = code.s[0], Up = code.s[j]; //左插头 上插头int dp = Hash[k].dp[lst];if( ! mp[i][j] ) {if( ! Left and ! Up ) insert( Zip( code ), dp );}else if( ! Left and ! Up ) {if( mp[i][j] == 1 and mp[i + 1][j] and mp[i][j + 1] ) {code.s[0] = 2, code.s[j] = 1;insert( Zip( code ), dp );}}else if( ! Left and Up ) {if( ( mp[i][j] == 3 or mp[i][j] == 1 ) and mp[i + 1][j] ) insert( Zip( code ), dp );if( mp[i][j] == 1 and mp[i][j + 1] ) {code.s[0] = Up, code.s[j] = 0;insert( Zip( code ), dp );}}else if( Left and ! Up ) {if( ( mp[i][j] == 1 or mp[i][j] == 2 ) and mp[i][j + 1] ) insert( Zip( code ), dp );if( mp[i][j] == 1 and mp[i + 1][j] ) {code.s[0] = 0, code.s[j] = Left;insert( Zip( code ), dp );}}else if( Left == 1 and Up == 1 and mp[i][j] == 1 ) {//不属于同一个连通块 都是左括号//得连起来 然后与这两个左括号匹配的右括号中较近的一个改成左括号 方法使用括号匹配//显然肯定是左插头的匹配括号在右 呈包含关系int p, tot = 1;for( p = j + 1;p <= m;p ++ ) {if( code.s[p] == 1 ) tot ++;if( code.s[p] == 2 ) tot --;if( ! tot ) break;}code.s[0] = code.s[j] = 0, code.s[p] = 1;insert( Zip( code ), dp );}else if( Left == 2 and Up == 2 and mp[i][j] == 1 ) {//显然肯定是上插头的匹配括号在左 呈包含关系int p, tot = -1;for( p = j - 1;p;p -- ) {if( code.s[p] == 1 ) tot ++;if( code.s[p] == 2 ) tot --;if( ! tot ) break;}code.s[0] = code.s[j] = 0, code.s[p] = 2;insert( Zip( code ), dp );}else if( Left == 2 and Up == 1 and mp[i][j] == 1 ) {code.s[0] = code.s[j] = 0;insert( Zip( code ), dp );}else if( Left == 1 and Up == 2 and mp[i][j] == 1 ) { //回路形成code.s[0] = code.s[j] = 0; bool flag = 0;//判断是否合法for( int p = 0;p <= m;p ++ )if( code.s[i] ) { flag = 1; break; }if( ! flag and i == Endx and j == Endy ) ans += dp;}}}}printf( "%lld\n", ans );return 0;
}

ParkII

BZOJ2310

这道题就是求一条简单路径,那么就不要求一定是个回路,这个时候如果采取括号表示法讨论就有点多了。

最小表示法就较好写一点,多压一个路径的端点数量即可,最后的合法结果一定有且只有两个端点。也就是单独插头。

#include <bits/stdc++.h>
using namespace std;
#define mod 298999
#define ll long long
int n, m, num, lst, now; //num:单独插头的个数
int cnt[2], head[300000];
int v[105][10];struct HashTable { int sta[2], nxt;ll dp[2]; }Hash[300000];
void insert( int sta, ll val ) {int key = sta % mod;for( int i = head[key];i;i = Hash[i].nxt )if( Hash[i].sta[now] == sta ) {Hash[i].dp[now] = max( val, Hash[i].dp[now] );return;}++ cnt[now];Hash[cnt[now]].sta[now] = sta;Hash[cnt[now]].dp[now] = val;Hash[cnt[now]].nxt = head[key];head[key] = cnt[now];
}struct node { int s[15]; };
int vis[10];
int Zip( node code ) {int sta = 0, tot = 0;for( int i = 1;i <= 6;i ++ ) vis[i] = 0;for( int i = 0;i <= m;i ++, sta <<= 3 ) {if( ! code.s[i] ) continue;if( ! vis[code.s[i]] ) vis[code.s[i]] = ++ tot; //找最小表示sta |= vis[code.s[i]];}sta |= num;return sta;
}
node unZip( int sta ) {node code;num = sta & 7;for( int i = m;~ i;i -- )sta >>= 3, code.s[i] = sta & 7;return code;
}int main() {scanf( "%d %d", &n, &m );for( int i = 1;i <= n;i ++ )for( int j = 1;j <= m;j ++ )scanf( "%d", &v[i][j] );insert( 0, 0 );for( int i = 1;i <= n;i ++ )for( int j = 1;j <= m;j ++ ) {lst = now, now ^= 1, cnt[now] = 0;memset( head, 0, sizeof( head ) );for( int k = 1;k <= cnt[lst];k ++ ) {node code = unZip( Hash[k].sta[lst] );ll dp = Hash[k].dp[lst] + v[i][j];int left = code.s[0], up = code.s[j];if( left and up ) {if( left ^ up ) { //两个插头不是同一个连通块 在(i,j)位置合并两个连通块code.s[0] = code.s[j] = 0;//横着和竖着的轮廓线都要被擦掉for( int p = 0;p <= m;p ++ )if( code.s[p] == up ) code.s[p] = left;insert( Zip( code ), dp );}}else if( left or up ) { //合并两种情况int id = left + up;if( i < n ) {code.s[j] = id, code.s[0] = 0;insert( Zip( code ), dp );}if( j < m ) {code.s[0] = id, code.s[j] = 0;insert( Zip( code ), dp );}if( num < 2 ) {code.s[j] = code.s[0] = 0, num ++;insert( Zip( code ), dp );}}else {insert( Zip( code ), Hash[k].dp[lst] );//两边都没有插头 可以选择不选这个格子if( i < n and j < m ) {//一个新连通块的中转点code.s[j] = code.s[0] = 6;//用最大的填 保证编号不与已存在的连通块冲突insert( Zip( code ), dp );}if( num < 2 ) { //直接做一个单独插头 不是路径的起点就是终点num ++;if( i < n ) {code.s[j] = 6, code.s[0] = 0;insert( Zip( code ), dp );}if( j < m ) {code.s[j] = 0, code.s[0] = 6;insert( Zip( code ), dp );}}}}}ll ans = -1e18;for( int i = 1;i <= cnt[now];i ++ )if( ( Hash[i].sta[now] & 7 ) == 2 ) ans = max( ans, Hash[i].dp[now] );printf( "%lld\n", ans );return 0;
}

Tony’s Tour

还要简单一点,稍微转化一下,在棋盘最后加入两行。

.####.

. . . . . .

就让起点和终点联通形成回路了,且这两行的走法之有一种。

#include <bits/stdc++.h>
using namespace std;
#define mod 298999
bool mp[15][15];
int head[300000], cnt[2];
int lst, now, n, m;struct HashTable { int sta[2], dp[2], nxt; }Hash[300000];
void insert( int sta, int val ) {int key = sta % mod;for( int i = head[key];i;i = Hash[i].nxt )if( Hash[i].sta[now] == sta ) {Hash[i].dp[now] += val;return;}++ cnt[now];Hash[cnt[now]].sta[now] = sta;Hash[cnt[now]].dp[now] = val;Hash[cnt[now]].nxt = head[key];head[key] = cnt[now];
}struct node { int s[15]; };
int Zip( node code ) {int sta = 0;for( int i = 1;i <= m;i ++ ) sta |= ( code.s[i] << ( i << 1 ) );sta |= code.s[0];return sta;
}
node unZip( int sta ) {node code;code.s[0] = sta & 3;for( int i = 1;i <= m;i ++ ) code.s[i] = ( sta >> ( i << 1 ) ) & 3;return code; 
}int main() {char ch[15];while( scanf( "%d %d", &n, &m ) and n and m ) {memset( mp, 0, sizeof( mp ) ); //因为多组数据且后面没有判断是否(i+1,j)(i,j+1)在棋盘内 所以要全都清空 避免上一轮的mp有的地方为1for( int i = 1;i <= n;i ++ ) {scanf( "%s", ch + 1 );for( int j = 1;j <= m;j ++ )if( ch[j] == '.' ) mp[i][j] = 1;else mp[i][j] = 0;}mp[n + 1][1] = mp[n + 1][m] = 1;for( int i = 2;i < m;i ++ ) mp[n + 1][i] = 0;for( int i = 1;i <= m;i ++ ) mp[n + 2][i] = 1;n += 2;now = 0, cnt[now] = 0;memset( head, 0, sizeof( head ) );insert( 0, 1 ); int ans = 0;for( int i = 1;i <= n;i ++ )for( int j = 1;j <= m;j ++ ) {lst = now, now ^= 1, cnt[now] = 0;memset( head, 0, sizeof( head ) );for( int k = 1;k <= cnt[lst];k ++ ) {node code = unZip( Hash[k].sta[lst] );int left = code.s[0], up = code.s[j];int dp = Hash[k].dp[lst];if( ! mp[i][j] ) {if( ! left and ! up ) insert( Zip( code ), dp );}else if( ! left and ! up ) {if( mp[i + 1][j] and mp[i][j + 1] ) {code.s[0] = 2, code.s[j] = 1;insert( Zip( code ), dp );}}else if( ! left and up ) {if( mp[i + 1][j] ) insert( Zip( code ), dp );if( mp[i][j + 1] ) {code.s[0] = up, code.s[j] = 0;insert( Zip( code ), dp );}}else if( left and ! up ) {if( mp[i][j + 1] ) insert( Zip( code ), dp );if( mp[i + 1][j] ) {code.s[j] = left, code.s[0] = 0;insert( Zip( code ), dp );}}else if( left == 2 and up == 1 ) {code.s[0] = code.s[j] = 0;insert( Zip( code ), dp );}else if( left == 1 and up == 1 ) {int p, tot = 1;for( p = j + 1;p <= m;p ++ ) {if( code.s[p] == 1 ) tot ++;if( code.s[p] == 2 ) tot --;if( ! tot ) break;}code.s[0] = code.s[j] = 0, code.s[p] = 1;insert( Zip( code ), dp );}else if( left == 2 and up == 2 ) {int p, tot = -1;for( p = j - 1;p;p -- ) {if( code.s[p] == 1 ) tot ++;if( code.s[p] == 2 ) tot --;if( ! tot ) break;}code.s[0] = code.s[j] = 0, code.s[p] = 2;insert( Zip( code ), dp );}else if( left == 1 and up == 2 ) {code.s[0] = code.s[j] = 0; bool flag = 1;for( int p = 0;p <= m;p ++ )if( code.s[p] ) { flag = 0; break; }if( flag and i == n and j == m ) ans += dp;}}}printf( "%d\n", ans );}return 0;
}

Efficient Tree

也是轮廓线 DPDPDP 的题目,直接分类讨论就行。

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define mod 1000000007
struct node {int ans, cnt;node operator + ( node &now ) const {if( ans < now.ans ) return *this;else if( ans > now.ans ) return now;else return { ans, ( cnt + now.cnt ) % mod };}node operator + ( int x ) { return { ans + x, cnt }; }node operator * ( int x ) { return { ans, cnt * x % mod }; };
};
int L[1000][1000], U[1000][1000];
int lst, now, n, m;int cnt[2], head[2500];
struct HASH { node dp[2]; int sta[2], nxt; }Hash[3000000];
void insert( int sta, node cost ) {int key = sta % 2333;for( int i = head[key];i;i = Hash[i].nxt )if( Hash[i].sta[now] == sta ) {Hash[i].dp[now] = Hash[i].dp[now] + cost;return;}++ cnt[now];Hash[cnt[now]].sta[now] = sta;Hash[cnt[now]].dp[now] = cost;Hash[cnt[now]].nxt = head[key];head[key] = cnt[now];
}int vis[10];
struct zip { int s[15];zip(){ memset( s, 0, sizeof( s ) ); }/*注意清空因为第i行最后1列跳转到第i+行第1列的时候会访问到第0列的状态 呼应flag单独联通块的判断*/
};
int encode( zip code ) {int sta = 0, tot = 0;memset( vis, -1, sizeof( vis ) );vis[0] = 0;for( int i = 1;i <= m;i ++ ) {if( vis[code.s[i]] == -1 ) vis[code.s[i]] = ++ tot;sta = ( sta << 3 ) | vis[code.s[i]];}return sta;
}
zip decode( int sta ) {zip code;for( int i = m;i;i --, sta >>= 3 ) code.s[i] = sta & 7;return code;
}signed main() {int T;scanf( "%lld", &T );for( int t = 1;t <= T; t++ ) {scanf( "%lld %lld", &n, &m );for( int i = 1;i <= n;i ++ )for( int j = 2;j <= m;j ++ )scanf( "%lld", &L[i][j] );for( int i = 2;i <= n;i ++ )for( int j = 1;j <= m;j ++ )scanf( "%lld", &U[i][j] );now = 0, cnt[now] = 0;memset( head, 0, sizeof( head ) );insert( 0, { 0, 1 } );for( int i = 1;i <= n;i ++ )for( int j = 1;j <= m;j ++ ) {lst = now, now ^= 1, cnt[now] = 0;memset( head, 0, sizeof( head ) );for( int k = 1;k <= cnt[lst];k ++ ) {zip code = decode( Hash[k].sta[lst] );int left = code.s[j - 1], up = code.s[j];node dp = Hash[k].dp[lst];bool flag = 1;for( int p = 0;p <= m;p ++ )if( p ^ j and code.s[p] == up ) { flag = 0; break; }if( j > 1 and ! flag ) {code.s[j] = left;insert( encode( code ), { dp.ans + L[i][j], dp.cnt * 2 % mod } );code.s[j] = up;}if( i > 1 )insert( encode( code ), { dp.ans + U[i][j], dp.cnt * 2 % mod } );if( ! flag ) {code.s[j] = 8; //赋值成不可能有的最小表示法到达的连通块编号数量 表示新开一个连通块insert( encode( code ), dp );code.s[j] = up;}if( i > 1 and j > 1 and left ^ up ) {for( int p = 0;p <= m;p ++ ) if( code.s[p] == left ) code.s[p] = up;insert( encode( code ), { dp.ans + U[i][j] + L[i][j], dp.cnt * 3 % mod } );}}}node ans = { 0x7f7f7f7f, 0 };for( int i = 1;i <= cnt[now];i ++ ) {bool flag = 1;zip code = decode( Hash[i].sta[now] );for( int j = 1;j < m;j ++ ) flag &= ( code.s[j] == code.s[j + 1] );if( flag ) ans = ans + Hash[i].dp[now];}printf( "Case #%lld: %lld %lld\n", t, ans.ans, ans.cnt );}return 0;
}

[CQOI2015]标识设计

BZOJ3934
LLL 形状的表示拆分成 一个只有下插头,一段上下插头,一个有上插头和右插头,一段左右插头,一个只有左插头 五个部分。

记录转折插头的个数和只有左插头的个数。

当转折插头个数有 333 个,只有左插头的个数已经有 222 个了。当现在这个格子成为左插头就形成了 333LLL 标识。统计进入答案即可。

当然这一切都要在合法的情况下进行插头转移。

#include <bits/stdc++.h>
using namespace std;
#define int long long
int lst, now, ans;
int cnt[2], head[300000];
bool ch[50][50];struct HashTable { int sta[2], dp[2], nxt; }Hash[700000];
void insert( int sta, int val ) {int key = sta % 299989;for( int i = head[key];i;i = Hash[i].nxt )if( Hash[i].sta[now] == sta ) {Hash[i].dp[now] += val;return;}++ cnt[now];Hash[cnt[now]].sta[now] = sta;Hash[cnt[now]].dp[now] = val;Hash[cnt[now]].nxt = head[key];head[key] = cnt[now];
}int zip( int x, int y, int z ) {return ( x << 4 ) | ( y << 2 ) | z; }signed main() {int n, m; char s[50];scanf( "%lld %lld", &n, &m );for( int i = 1;i <= n;i ++ ) {scanf( "%s", s + 1 );for( int j = 1;j <= m;j ++ )ch[i][j] = s[j] == '.';}insert( 0, 1 );for( int i = 1;i <= n;i ++ ) {for( int k = 1;k <= cnt[now];k ++ )Hash[k].sta[now] = (Hash[k].sta[now] & 15) | (Hash[k].sta[now] >> 4 << 5);for( int j = 1;j <= m;j ++ ) {lst = now, now ^= 1, cnt[now] = 0;memset( head, 0, sizeof( head ) );for( int k = 1;k <= cnt[lst];k ++ ) {int sta = Hash[k].sta[lst] >> 4, dp = Hash[k].dp[lst];int left = sta >> j - 1 & 1, up = sta >> j & 1;int num = Hash[k].sta[lst] >> 2 & 3;int tot = Hash[k].sta[lst] & 3;if( up and left ) continue; //这个格子连出去两个格子 不合法 形成的是反L形else if( ! ch[i][j] ) {if( ! left and ! up ) insert( zip(sta, num, tot), dp );}else if( ! left and ! up ) {insert( zip(sta, num, tot), dp );if( num < 3 and ch[i + 1][j] ) insert( zip((1 << j - 1) | sta, num + 1, tot), dp );}else if( ! left and up ) {if( ch[i][j + 1] ) insert( zip(sta, num, tot), dp );if( ch[i + 1][j] ) insert( zip(sta ^ (1 << j - 1) ^ (1 << j), num, tot), dp );}else {if( ch[i][j + 1] ) insert( zip(sta ^ (1 << j - 1) ^ (1 << j), num, tot), dp );if( num == 3 and tot == 2 ) ans += dp;else insert( zip(sta ^ (1 << j - 1), num, tot + 1), dp );}}}}printf( "%lld\n", ans );return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/316664.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

H - Maximal submatrix HDU - 6957

H - Maximal submatrix HDU - 6957 题意&#xff1a; 给定一个n行m列的矩阵&#xff0c;求每列上面积不减的最大子矩阵 对于每个测试用例&#xff0c;打印一个表示最大子矩阵的整数 题解&#xff1a; 要求求一个最大面积的满足每列非递减的矩阵&#xff0c;这怎么想&#…

[NewLife.XCode]实体类详解

NewLife.XCode是一个有10多年历史的开源数据中间件&#xff0c;由新生命团队(2002~2019)开发完成并维护至今&#xff0c;以下简称XCode。整个系列教程会大量结合示例代码和运行日志来进行深入分析&#xff0c;蕴含多年开发经验于其中。开源地址&#xff1a;https://github.com/…

CF662C Binary Table(FWT_XOR卷积)

problem 洛谷链接 solution 第二次做的时候发现自己还是不会。发现自己没有写过题解&#xff0c;看来当时是没有完全搞懂的。 nnn 与 mmm 的量级相差很大&#xff0c;nnn 的范围是完全可以状压的。 不妨考虑枚举最后翻转了哪些行&#xff0c;将操作状压为一个数 XXX。 显然…

AT5662 [AGC040D] Balance Beam(二分)

前言 人类智慧&#xff0c;不可思议。 解析 考虑画出两个人的 S−TS-TS−T 折线图&#xff0c;那么答案如何表示&#xff1f; 可以理解成把 BBB 的图像不断下移&#xff0c;直到与 AAA 的图像只剩一个交点&#xff0c;此时在 xxx 轴的截矩就是答案。 设平移后 BBB 图像与 x…

Counting Triangles

Counting Triangles 题意&#xff1a; 给你一个完全图&#xff0c;每个边被赋值为0或1&#xff0c;问这个完全图中有多少个完美三角形&#xff1f; 完美三角形定义&#xff1a;三角形的三边都为0或1 题解&#xff1a; 正着求不好求&#xff0c;我们可以倒着想 不考虑完美&a…

程序员过关斩将--论商品促销代码的优雅性

点击上方蓝色字体&#xff0c;关注我们菜菜哥&#xff0c;YY说你帮她解决了几个问题&#xff0c;也帮我解决一个呗原来是D妹子&#xff0c;来坐我身边&#xff0c;说下情况我的项目是个电商项目&#xff0c;现在产品狗要给商品做活动正常呀我一个新手初来咋到顶不住压力了&…

[WC2018]州区划分(FWT_OR卷积)

problem 洛谷链接 solution 显然题目指向&#xff1a;存在欧拉回路的州划分是不合法的。 当且仅当这个州是 联通 的且 内部没有奇数度数的点 时&#xff0c;这个州不合法。 因为 nnn 非常小&#xff0c;我们可以枚举每一种州划分方案&#xff0c;判断是否合法&#xff0c;…

P5469 [NOI2019] 机器人(拉格朗日插值、区间dp)

解析 打表可得&#xff0c;有效状态大概只有 O(m)O(nlog⁡n)O(m)O(n\log n)O(m)O(nlogn) 种。 枚举最靠右的最大值位置&#xff0c;不难得到 O(mV)O(mV)O(mV) 的做法。 期望得分 505050 分。 考虑如何做 l0,r109l0,r10^9l0,r109。&#xff0c;发现前缀和后所有的 dpi,i,xxdp_…

Math(牛客多校第三场)

Math 题意&#xff1a; 问你有多少对(x,y),1<x<y<n,满足(x2 y2)%(xy1) 0 题解&#xff1a; 这种题。。。直接打表芜湖~ 通过打表发现&#xff1a;满足情况的为(i,i * i * i),但是也有不和谐的声音出现&#xff1a;当x8时&#xff0c;会出现两个&#xff0c;一个…

[NewLife.XCode]增删改查入门

NewLife.XCode是一个有10多年历史的开源数据中间件&#xff0c;由新生命团队(2002~2019)开发完成并维护至今&#xff0c;以下简称XCode。整个系列教程会大量结合示例代码和运行日志来进行深入分析&#xff0c;蕴含多年开发经验于其中。开源地址&#xff1a;https://github.com/…

P5472 [NOI2019] 斗主地(期望、数学)

前言 我咋连表都没打啊。 too vegetable。 解析 题目给出的洗牌形式看着并不好看&#xff0c;合理猜测可以发现&#xff0c;这其实就等价于所有可能情况等概率出现。 然后就不会了 打表可以发现&#xff1a;当 tp1 时&#xff0c;dp 数组是一个等差数列。当 tp2 时&#xff…

[HNOI2012]集合选数(思维构造 + 状压dp)

problem 题目链接 solution 从最小一个数 xxx 开始&#xff0c;将其 2x,3x2x,3x2x,3x 放入&#xff0c;再将 2(2x),3(2x),2(3x),3(3x)2(2x),3(2x),2(3x),3(3x)2(2x),3(2x),2(3x),3(3x) 放入&#xff0c;以此类推 …\dots… 将其合并为一个集合。重复又找一个最小未进入集合的…

在实际项目中使用LiteDB NoSQL数据库

LiteDB 是一个 NoSQL 数据库&#xff0c;特点是 MongoDB like 和 0 配置。100% 原汁原味的 C# 开发, Release 只有一个 DLL&#xff0c;官方有一下适用场景&#xff1a;移动App&#xff0c;桌面小应用程序&#xff0c;特有的文件格式&#xff0c;小型的 Web 应用&#xff0c;需…

2021牛客暑期多校训练营3

2021牛客暑期多校训练营3 题号题目知识点AGuess and liesBBlack and whiteCMinimum grid二分图匹配DCountEMath数论打表找规律F24dian模拟GYu Ling(Ling YueZheng) and Colorful TreeHLing Qiu, Luna and Triple BackpackIKuriyama Mirai and Exclusive OrJCounting Triangles…

P4769 [NOI2018] 冒泡排序(组合数学)

前言 这里是线性做法。 在题解里几句话说清楚的性质愣是推了一上午。 too vegetable 解析 考虑怎样的排列是不合法的。 一个排列如果不合法&#xff0c;也就是在某次交换时其中一个元素距离目标的距离没有减少反而增大了&#xff0c;那么以后这个数一定会再换回来&#xff0…

[骗分技巧——随机化Ⅰ]CodeChef-Milestones,CF364D-Ghd

文章目录CodeChef-MilestonesproblemsolutioncodeCF364D-Ghdproblemsolutioncode设随机化一次的正确率为 ppp&#xff0c;一次的复杂度为 O(f(n))O(f(n))O(f(n))。则随机的期望次数 kkk&#xff1a;k∑i1∞p(1−p)i−1i(1)(1−p)k∑i1∞p(1−p)ii∑i2∞p(1−p)i−1(i−1)(2)(1)…

初探奥尔良(Orleans)

由于工作上关系目前经常被各种并发数据问题搞得焦头烂额&#xff0c;要么要性能舍弃数据上得一致性&#xff0c;要么要一致性但是却得到了特别糟糕的响应。难道鱼和熊掌真的无法兼得吗&#xff1f;然后找到了类似奥尔良这种基于Actor模型的kuangjia首先本人因为是C#系的所以暂不…

2021杭电多校1

题号题目知识点AMod, Or and Everything思维数论BRocket landCPuzzle loopDAnother thief in a ShopEMinimum spanning tree推结论欧拉筛FXor sum01字典树GPass!HMaximal submatrix单调栈IKD-GraphJzotoKNecklace of Beads

P4775 [NOI2018] 情报中心(线段树合并)

前言 似乎也没有那么难&#xff1f; 但确实也不太好想。 解析 对于两条有交路径 (u1,v1,c1),(u2,v2,c2)(u_1,v_1,c_1),(u_2,v_2,c_2)(u1​,v1​,c1​),(u2​,v2​,c2​)&#xff0c;设 tlca(u1,u1)tlca(u_1,u_1)tlca(u1​,u1​) 为四个 lca 中最深的&#xff0c;那么代价的二…

[骗分技巧——随机化Ⅱ] [Poi2014]Couriers,CodeChef - TKCONVEX

文章目录[Poi2014]CouriersproblemsolutioncodeCodeChef - TKCONVEXproblemsolutioncode随机算法的典型套路&#xff1a;枚举太花时&#xff0c;转化为随机一个数。然后通过对正确率的分析&#xff0c;选择一个随机的次数来卡。前提是要保证每一次检验随机是否为答案的时间复杂…