轮廓线dp、插头dp

轮廓线dp

轮廓线:对于一个n*m棋盘上的决策,当我们考虑到(i,j)这个格子时,第i行前j-1个格子的下边以及第j个格子的左边以及第j到第m-1-j个格子的上边所组成的一条线,我们称之为轮廓线

轮廓线dp:当我们在棋盘第(i,j)格子上放置棋子时,我们的决策受到且仅受到(i,j)格子左边以及上边的状态的影响,我们这个时候就可以 采取轮廓线dp的方法来解决

例题:P5074 Eat the Trees - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题目大意:给出n*m的棋盘,其中为1的格子必须铺线,为0的格子禁止铺线,棋盘上可形成多条回路,求所有合法方案数

具体思路:对于一个格子而言,我们的线的走向有且仅有6种情况,用1-4表示一个格子的4条边,(a,b)二元对表示这个格子的线跨越哪两条边,那么6种情况分别为:(1,2),(1,3),(1,4),(2,3),(2,4),(3,4)。我们考虑轮廓线dp:当边被线跨越时,我们将这条边记为1,否则,我们将这条边记为0。

更普遍地说,我们将该边被跨越,称为这条边上有插头,反之,我们称这条边上无插头

那么一条轮廓线的状态,我们就可以用一个m+1位的二进制数来表示

现在来考虑轮廓线的状态(下面称之为s)如何转移:

先考虑一般情况,我们现在考虑第(i,j)个格子,s的第j位表示该格子的左边是否有插头,我们记为d1,j+1位表示该格子的上边是否有插头,我们记为d2。由位运算我们可以知道,d1=s>>j&1;d2=s>>j+1&1;

当我们更新完第i行第j列的格子的状态时,其下边和右边的插头状态就会被更新,而由于我们下一个dp的就是第j+1格,对于j+1格而言,第j位表示的就是第j格下边的插头,第j+1格表示的就是第j+1格左边的插头,也就是第j格右边的插头。因此,我们在更新第j个格子的插头时,第j位的状态从表示第j格左边插头更新到表示第j格下边插头,第j+1位的状态从表示第j格上边插头更新到表示第j格右边插头。

我们现在考虑d1和d2会有以下几种情况:

当该格子能铺线时:

1.当d1无插且d2无插时,这时说明之前的回路已闭合,我们要手动创造一个回路的开头,也就是将(i,j)格子的下边和右边都加上插头,也就是把d1从0更新到1,d2从0更新到1,也就是从状态s转移到了状态s^3<<j

2.当d1有插且d2无插时,为了保证插头的连通性,我们必须要把d1插头进行延伸,可以将其插向下边,也可以将其插向右边,也就是把d1更新为1、d2更新为0或者d1更新为0,d2更新为1,也就是从状态s转移到状态s以及从状态s转移到s^3<<j

3.当d1无插且d2有插时,同上,从状态s转移到状态s以及从状态s转移到状态s^3<<j

4.当d1有插且d2有插,这个时候我们只能把d1延伸出来的线和d2延伸出来的线连在一起,形成一个回路,那么这个时候第j格的下边和右边将不会有插头出现,也就是把d1从1更新到0,把d2从1更新到0,也就是从状态s转移到状态s^3<<j

当该格子不能铺线时:

此时若该格子的左边或者上边有插头的话,就无法继承其延伸出来的线,也就是这种状态下必不可能转移到一种合法的状态,因此这个时候我们不继承其方案数。那么当且仅当d1无插且d2无插的时候,我们我们这个格子才可以不铺线也不会影响方案的合法性,也就是从状态s转移到状态s

以上为在第i行内的状态转移情况,那么我们现在再来考虑行间如何进行状态转移

当我们dp第i行的第0个格子时,状态s的第0位表示的是该格子左边的插头情况,且其只能为0,(第0格左边不可能出现插头),而第1位表示的是该格子下边的插头。当我们dp到第i行的最后一个,也就是第m-1个格子时,我们会把第m-1位更新为第m-1个格子的下边的插头,而把第m位更新为第m-1个格子的右边的插头 ,此时,我们也希望且一定要,右边的插头不存在,否则就不是合法方案

因此,我们在完成对第i行的状态转移,将转移到第i+1行时,由于第0位已经更新为了第i行第0个格子下边的插头,也就是第i+1行第0个格子上边的插头,因此,我们应当将这个状态s整体向左移一位,这样,第1位又重新表示回了第0个格子上方的插头(同时往后的第2到第m位也同理),而第0位也被左移初始化为0,表示第0个格子左边的插头,且一定没有插头。同时,原来的第m位(表示右插的)被左移到第m+1位,如果它原本为1的话,那么由于我们遍历状态s的时候,只遍历到第m位就结束了,也就是从0到(1<<m+1)-1那么这个右插存在的不合法状态一定不会被我们遍历到,也就是不会被转移,我们就成功将其筛出考虑范围外了。

总结的话就是,在行间转移的时候,我们将所有状态向左移1位更新即可

实现代码:

#include <iostream>
#include <cstdio>
using namespace std;
const int M = 14, N = 1 << 14;
#define limit (1 << m + 1)
//滚动数组
unsigned long long dp[2][N], tmp[N];
bool book[M][M];
int T, n, m, now, pre;
int main()
{
    cin >> T;
    while (T--)
    {
        cin >> n >> m;
        for (int i = 1; i <= n; i++)
            for (int j = 0; j < m; j++)
            {
                book[i][j] = false;
                cin >> book[i][j];
            } // true为能铺
        for (int i = 0; i < limit; i++) dp[0][i] = 0;
        dp[0][0] = 1;
        now = 1; pre = 0;
        //遍历第k行
        for (int k = 1; k <= n; k++)
        {
            // 换行处理
            for (int i = 0; i < limit; i++)
            {
                tmp[i] = dp[pre][i];
                dp[pre][i] = 0;
            }

            for (int i = 0; i < limit; i++)
                dp[pre][i << 1] = tmp[i];
            //遍历第j格
            for (int j = 0; j < m; j++)
            {
                //初始化为0
                for (int s = 0; s < limit; s++) dp[now][s] = 0;
                //枚举状态s
                for(int s=0;s<limit;s++){
                    //不能铺
                    if(!book[k][j]) {if(!(s>>j&3)) dp[now][s]+=dp[pre][s];}
                    //能铺
                    else{
                        if((s>>j&3)==2 || (s>>j&3)==1) dp[now][s]+=dp[pre][s];
                        dp[now][s^3<<j]+=dp[pre][s];
                    }
                }
                now ^= 1;
                pre ^= 1;
            }
        }
        cout << dp[pre][0] << endl;
    }
    return 0;
}

插头dp:

题目链接:P5056 【模板】插头 DP - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题目和上面的题目基本一样,唯一的区别是棋盘上只能形成一条回路

我们考虑如何限制只能形成一条回路这个条件:

我们来思考为何刚刚的做法可以形成多条回路? 是因为我们可以创建多种流通线并可以随时闭合

那么我们来考虑一条回路的特点:对于一条回路而言,我们可以任意选取其的一个起点以及一个终点,我们发现,从其起点左边到终点是一条线直通,起点右边到终点也是一条线直通。因此,我们只需要在dp搭线的时候也如此限制就好了。

括号表示法:

我们将之前的插头进行细分,分为左插头和右插头,左插头就是回路向左边延伸的插头,右插头就是回路向右边延伸的插头

这里用到一个定理:当一条轮廓线上,存在着且仅有这样的排序:左插1,左插2,右插2,右插1时,我们可以保证左插1是与右插1匹配,而左插2与右插2匹配,若左插1与右插2匹配,则这两条回路一定会在上方相交从而矛盾

因此,一条边上的状态就有了3种,我们可以用2位二进制来表示插头状态,分别为:

00(0):无插,01(1):左插,10(2):右插,11(3):无意义

下面我们再来考虑状态转移:

首先是行间状态转移和上题是一样的,只不过这里变成了左移2位

然后是逐格转移:

考虑dp到该行第j格,d1表示该格左边的插头,而d2表示该格上边的插头

那么d1=s>>j*2&3;d2=s>>(j+1)*2&3;

有以下几种情况:

首先是该格子能铺线时:

1.d1无插且d2无插:这时要将第j格的下边更新为左插,把右边更新为右插,即从状态s转移到状态s^1<<j*2^2<<(j+1)*2, (也可以写成s^9<<j*2)

2.d1为左插且d2无插或d1为右插且d2无插:这时可以把插头向下边延伸或者向右边延伸,即从状态s转移到状态s或者从状态s转移到状态s^d1<<j*2^d1<<(j+1)*2

3.d1无插且d2为左插或d1无插且d2为右插:这时可以把插头向下边延伸或向右边延伸,即从状态s转移到状态s或者从状态s转移到状态s^d2<<j*2^d2<<(j+1)*2

4.d1为左插且d2为左插,这个时候要把d1和d2两边的插头连接,且由于两个左插连接,那么d2所对应的右插就应该要更新为左插,才能保证d1所对应的右插有左插对应,假设找到d2所对应的右插(向高位找)在第k/2个格子,那么就是从状态s转移到状态s^1<<j*2^1<<(j+1)*2^3<<k,(也可以写成s^5<<j*2^3<<k)

5.d1为右插且d2为右插,这个时候要把d1和d2两边的插头连接,且要把d1所对应的左插更新为右插,才能保证d2所对应的左插有右插对应,假设找到d1所对应的左插(向低位找)在第k/2个格子,那么就是从状态s转移到状态s^2<<j*2^2<<(j+1)*2^3<<k,(也可以写成s^10<<j*2^3<<k)

6.d1为右插且d2为左插,这种情况我们直接把d1和d2两边的插头连接即可,也就是从状态s转移到状态s^d1<<j*2^d2<<(j+1)*2,(也可以写成s^6<<j*2)

7.d1为左插且d2为右插,这种情况我们的回路就要闭合了,所以当且仅当我们dp到最后一格终点的时候,我们才能继承这个状态的合法方案数

然后是该格子不能铺线的时候:

和上题一样,当且仅当d1和d2都无插的时候,从状态s继承到状态s 

最后,由于我们需要枚举的数字高达1<<2*(m+1),我们要使用哈希表进行枚举优化,仅枚举上次状态转移保存下来的合法方案数即可,保存合法状态时,要注意放下插头的所对应的下一格是否能铺线,不能铺线的话,我们就不保存该合法状态

实现代码:

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int n, m, ex, ey, now, pre;
const int M = 15, N = 1 << 26, mod = 299987;
bool book[M][M]; char str[M];
int head[300000], nxt[N], que[2][N], cnt[2];
long long val[2][N], ans;
//book[i][j]:记录第i行第j列是否能铺线
//head:哈希表表头
//que:保存合法状态
//cnt:合法状态数
//val:合法状态对应的合法方案数
void read() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> str;
        for (int j = 0; j < m; j++) {
            if (str[j] == '.') { book[i][j] = true; ex = i; ey = j; }
        }
    }
}
//插入哈希表
void insert(int sta, long long num) {
    int u = sta % mod + 1;
    for (int i = head[u]; i; i = nxt[i]) {
        if (que[now][i] == sta) {
            val[now][i] += num;
            return;
        }
    }
    nxt[++cnt[now]] = head[u];
    head[u] = cnt[now];
    que[now][cnt[now]] = sta;
    val[now][cnt[now]] = num;
}
void solve() {
    cnt[now] = 1; val[now][1] = 1; que[now][1] = 0;//起初的合法状态就是全空
    for (int i = 1; i <= n; i++) {
        //换行处理,左移2位
        for (int s = 1; s <= cnt[now]; s++) que[now][s] <<= 2;

        for (int j = 0; j < m; j++) {
            memset(head, 0, sizeof(head));
            pre = now; now ^= 1;
            cnt[now] = 0;
            //遍历队列内的合法状态
            for (int k = 1, sta, d1, d2; k <= cnt[pre]; k++) {

                sta = que[pre][k]; long long num = val[pre][k];

                d1 = sta >> 2 * j & 3; d2 = sta >> 2 * (j + 1) & 3;

                //0:无插 1:左插 2:右插 3:无意义
                //能铺
                if (book[i][j]) {
                    //无插
                    if (!d1 && !d2) { if (book[i + 1][j] && book[i][j + 1]) insert(sta ^ 9 << j * 2, num); }
                    //无+左,无+右
                    else if (!d1 && d2) {
                        if (book[i][j + 1]) insert(sta, num);
                        if (book[i + 1][j]) insert(sta ^ d2 << j * 2 ^ d2 << 2 * (j + 1), num);
                    }
                    //左+无,右+无
                    else if (d1 && !d2) {
                        if (book[i + 1][j]) insert(sta, num);
                        if (book[i][j + 1]) insert(sta ^ d1 << j * 2 ^ d1 << 2 * (j + 1), num);
                    }

                    //右+左
                    else if (d1 == 2 && d2 == 1) insert(sta ^ 6 << j * 2, num);

                    //右+右
                    else if (d1 == 2 && d2 == 2) {
                        int check = 0;
                        //向低位找
                        for (int s = 2 * j; s >= 0; s -= 2) {
                            if ((sta >> s & 3) == 2) check++;
                            else if ((sta >> s & 3) == 1) check--;
                            if (!check) { insert(sta ^ 10 << j * 2 ^ 3 << s, num); break; }
                        }
                    }

                    //左+左
                    else if (d1 == 1 && d2 == 1) {
                        int check = 0;
                        //向高位找
                        for (int s = j * 2+2; s < 2 * (m + 1); s += 2) {
                            if ((sta >> s & 3) == 1) check++;
                            else if ((sta >> s & 3) == 2) check--;
                            if (!check) { insert(sta ^ 5 << j * 2 ^ 3 << s, num); break; }
                        }
                    }

                    //左+右
                    else if (i == ex && j == ey) ans += num;
                }
                else {
                    if (!d1 && !d2) insert(sta, num);
                }
            }
        }
    }
}

int main() {
    read();
    solve();
    cout << ans;
    return 0;
}

  • 14
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值