【学习笔记】简单的连通性状压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,一经查实,立即删除!

相关文章

[NewLife.XCode]实体类详解

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

Counting Triangles

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

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

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

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/…

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

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

初探奥尔良(Orleans)

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

[NewLife.XCode]功能设置

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

[NewLife.XCode]数据模型文件

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

[NewLife.XCode]高级增删改

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

[NewLife.XCode]数据初始化

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

[NewLife.XCode]反向工程(自动建表建库大杀器)

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

树上启发式合并

文章内容选自OI Wiki 参考博客 内容&#xff1a; 树上启发式合并&#xff08;dsu on tree&#xff09;对于某些树上离线问题可以速度大于等于大部分算法且更易于理解和实现的算法。 他是用来解决一类树上询问问题&#xff0c;一般这种问题有两个特征&#xff1a; 只有对子树…

Wexflow:C#中的开源工作流引擎

Wexflow是一个高性能、可扩展、模块化和跨平台的工作流引擎。Wexflow在GitHub&#xff1a;https://github.com/aelassas/Wexflow。Wexflow的目标是在没有用户干预的情况下自动执行重复任务。在Wexflow的帮助下&#xff0c;构建自动化和工作流过程变得简单。Wexflow还有助于使长…

ASP.NET Core 沉思录 - Logging 的两种介入方法

ASP.NET Core 中依赖注入是一个很重要的环节。因为几乎所有的对象都是由它创建的&#xff08;相关文章请参见《ASP.NET Core 沉思录 - ServiceProvider 的二度出生》&#xff09;。因此整个日志记录的相关类型也被直接添加到了 IServiceCollection 中。今天我们将介绍各个接口/…

C# 中的Async 和 Await 的用法详解

众所周知C#提供Async和Await关键字来实现异步编程。在本文中&#xff0c;我们将共同探讨并介绍什么是Async 和 Await&#xff0c;以及如何在C#中使用Async 和 Await。同样本文的内容也大多是翻译的&#xff0c;只不过加上了自己的理解进行了相关知识点的补充&#xff0c;如果你…

Docker的部署-包括网关服务(Ocelot)+认证服务(IdentityServer4)+应用服务

本文主要介绍通过Docker来部署通过.Net Core开发的微服务架构&#xff0c;部署的微服务主要包括统一网关&#xff08;使用Ocelot开发&#xff09;、统一认证&#xff08;IdentityServer4&#xff09;、应用服务&#xff08;asp.net core web api&#xff09;&#xff1b;本文不…

ASP.NET Core 实战:使用 Docker 容器化部署 ASP.NET Core + MySQL + Nginx

一、前言在之前的文章&#xff08;ASP.NET Core 实战&#xff1a;Linux 小白的 .NET Core 部署之路&#xff09;中&#xff0c;我介绍了如何在 Linux 环境中安装 .NET Core SDK / .NET Core Runtime、Nginx、MySQL&#xff0c;以及如何将我们的 ASP.NET Core MVC 程序部署到 Li…

VS2017 无法连接到Web服务器“IIS Express”终极解决方案

今天日了gou了&#xff0c;一大早打开VS2017的时候出现无法连接到Web服务器“IIS Express”的错误&#xff0c;然后必应了一下&#xff0c;再谷歌了一下找到的解决方法也都千篇一律&#xff0c;奈何都没能解决&#xff0c;最后通过静下心来的思考&#xff0c;尝试解决了问题&am…

Docker最全教程之使用.NET Core推送钉钉消息(二十)

前言上一篇我们通过实战分享了使用Go推送钉钉消息&#xff0c;由于技痒&#xff0c;笔者现在也编写了一个.NET Core的Demo&#xff0c;作为简单的对照和说明。最后&#xff0c;由于精力有限&#xff0c;笔者希望有兴趣的朋友可以分享下使用CoreRT将.NET Core编译成机器代码这块…