轮廓线DP - 学习笔记

轮廓线DP

1. 概述

1.1 什么是轮廓线DP

轮廓线DP是一种针对二维网格类问题的动态规划优化方法。当处理 \(n \times m\) 网格的逐行逐列决策问题时,其核心思想是用一个状态变量表示轮廓线的信息——即当前决策位置左侧和上方相关区域的状态,从而精准捕捉影响当前决策的所有前置条件。

轮廓线的定义:当处理到网格的 \((i,j)\) 位置时,状态信息包含两部分:

  • 第i行中,[0..j-1]列的决策状态(左侧已处理区域)
  • 第i-1行中,[j..m-1]列的决策状态(上方未覆盖区域)

这种状态表示方式能避免冗余计算,大幅提升效率。

1.2 轮廓线DP的优势

普通状压DP的困境:

  • 需用状态表示整行的决策状况,导致状态量庞大(通常为 \(2^m \times 2^m\)
  • 每行决策需通过DFS暴力枚举所有可能,时间复杂度高

轮廓线DP的改进:

  • 状态仅表示轮廓线信息,无需存储整行状态
  • 无需DFS枚举,决策过程随轮廓线推进逐步完成
  • 时间复杂度优化为 \(O(n\times m \times 2^{m})\),空间可通过滚动数组进一步压缩

2. 基础预备知识:位运算操作

轮廓线DP依赖位运算实现状态的快速获取与修改,核心操作如下:

操作目标 代码实现 说明
得到状态s第j位的状态 (s >> j) & 1 右移j位后与1,提取第j位二进制值
把状态s第j位设为1 s or (1 << j) 左移j位后与s或运算,保留其他位
把状态s第j位设为0 s & (~(1 << j)) 左移j位取反后与s与运算,仅第j位为0

注:若单个位置需表示更多状态(如染色问题的多颜色),可扩展为多位表示(如2位表示4种颜色)。

3. 经典题目解析

3.1 题目1:种草的方法数

3.1.1 问题描述

给定 \(n \times m\) 的二维网格grid\(0\) 表示不可种草,\(1\) 表示可种草),种草规则:任意两个种草的田地不能相邻(上、下、左、右),可自由决定种草数量,返回合法的种草方法数(答案对$ 1 \times 10^8$ 取模)。

约束:\(1 \le n, m \le 12\)

3.1.2 普通状压DP的困境

  • 状态需表示上一行的完整种草状况(\(2^m\) 种)
  • 当前行需枚举所有可能的种草状况(\(2^m\) 种)
  • 时间复杂度 \(O(n×2^m×2^m)\)\(m = 12\)\(2^{24} \approx 1.6\times 10^7\)\(n = 12\) 时总操作量达 \(1.9 \times 10^8\),接近时间上限

3.1.3 轮廓线DP思路

  • 状态s表示轮廓线:s[0..j-1]位是第i行左侧已处理列的种草状况,[j..m-1]位是第i-1行右侧未处理列的种草状况
  • 当前位置(i,j)的决策约束:
    1. grid[i][j]必须为1(可种草)
    2. 左侧列(j-1)未种草(sj-1位为 \(0\)
    3. 上方列(i-1,j)未种草(sj位为 \(0\)
  • 决策后更新轮廓线状态,继续处理右侧列

3.1.4 代码实现

版本1:状压DP
// Problem: P1879 [USACO06NOV] Corn Fields G
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1879
// Memory Limit: 125 MB
// Time Limit: 1000 ms

#include <cstdio>

const int N   = 15;
const int mod = 1e8;

int n, m;
int grid[ N ][ N ];
int dp[ N ][ 1 << N ];

int get(int s, int j) {
// 得到状态s中j位的状态
    return (s >> j) & 1;
}

// 状态s中j位的状态设置成v,然后把新的值返回
int set(int s, int j, int v) {
    return v == 0 ? (s & (~(1 << j))) : (s | (1 << j));
}

int f(int i, int s);

// 当前来到i行j列
// i-1行每列种草的状况s
// i行每列种草的状况ss
// 返回后续有几种方法
int dfs(int i, int j, int s, int ss) {
    if (j == m) {
        return f(i + 1, ss);
    }

    int ans = dfs(i, j + 1, s, ss);

    if (grid[ i ][ j ] == 1 && (j == 0 || get(ss, j - 1) == 0) && get(s, j) == 0) {
        ans = (ans + dfs(i, j + 1, s, set(ss, j, 1))) % mod;
    }

    return ans;
}

int f(int i, int s) {
    if (i == n) {
        return 1;
    }

    if (dp[ i ][ s ] != -1) {
        return dp[ i ][ s ];
    }

    int ans      = dfs(i, 0, s, 0);
    dp[ i ][ s ] = ans;

    return ans;
}

// 时间复杂度O(n * 2的m次方 * 2的m次方)
int compute() {
    for (int i = 0; i < n; ++i) {
        for (int s = 0; s < (1 << m); ++s) {
            dp[ i ][ s ] = -1;
        }
    }

    return f(0, 0);
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < m; ++j) {
            scanf("%d", &grid[ i ][ j ]);
        }
    }

    printf("%d", compute());

    return 0;
}
版本2:轮廓线DP
// Problem: P1879 [USACO06NOV] Corn Fields G
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1879
// Memory Limit: 125 MB
// Time Limit: 1000 ms

#include <cstdio>

const int N   = 15;
const int mod = 1e8;

int n, m;
int grid[ N ][ N ];
int dp[ N ][ N ][ 1 << N ];

int get(int s, int j) {
    return (s >> j) & 1;
}

int set(int s, int j, int v) {
    return v == 0 ? (s & (~(1 << j))) : (s | (1 << j));
}

// 当前来到i行j列
// i-1行中,[j..m-1]列的种草状况用s[j..m-1]表示
// i行中,[0..j-1]列的种草状况用s[0..j-1]表示
// s表示轮廓线的状况
// 返回后续有几种种草方法
int f(int i, int j, int s) {
    if (i == n) {
        return 1;
    }

    if (j == m) {
        return f(i + 1, 0, s);
    }

    if (dp[ i ][ j ][ s ] != -1) {
        return dp[ i ][ j ][ s ];
    }

    int ans = f(i, j + 1, set(s, j, 0));
    if (grid[ i ][ j ] == 1 && (j == 0 || get(s, j - 1) == 0) && get(s, j) == 0) {
        ans = (ans + f(i, j + 1, set(s, j, 1))) % mod;
    }
    dp[ i ][ j ][ s ] = ans;

    return ans;
}

// 时间复杂度O(n * 2的m次方 * m)
int compute() {
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < m; ++j) {
            for (int s = 0; s < (1 << m); ++s) {
                dp[ i ][ j ][ s ] = -1;
            }
        }
    }

    return f(0, 0, 0);
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < m; ++j) {
            scanf("%d", &grid[ i ][ j ]);
        }
    }

    printf("%d", compute());
    return 0;
}
版本3:轮廓线DP+空间压缩
// Problem: P1879 [USACO06NOV] Corn Fields G
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1879
// Memory Limit: 125 MB
// Time Limit: 1000 ms

#include <cstdio>
const int N   = 15;
const int mod = 1e8;

int n, m;
int grid[ N ][ N ];
int dp[ N ][ 1 << N ];
int prepare[ 1 << N ];

int get(int s, int j) {
    return (s >> j) & 1;
}

int set(int s, int j, int v) {
    return v == 0 ? (s & (~(1 << j))) : (s | (1 << j));
}

int compute() {
    for (int s = 0; s < (1 << m); ++s) {
        prepare[ s ] = 1;
    }

    for (int i = n - 1; i >= 0; --i) {
		// j == m
        for (int s = 0; s < (1 << m); ++s) {
            dp[ m ][ s ] = prepare[ s ];
        }
		
		// 普通位置
        for (int j = m - 1; j >= 0; --j) {
            for (int s = 0; s < (1 << m); ++s) {
                int ans = dp[ j + 1 ][ set(s, j, 0) ];
                if (grid[ i ][ j ] == 1 && (j == 0 || get(s, j - 1) == 0) && get(s, j) == 0) {
                    ans = (ans + dp[ j + 1 ][ set(s, j, 1) ]) % mod;
                }
                dp[ j ][ s ] = ans;
            }
        }
		
		// 设置prepare
        for (int s = 0; s < (1 << m); ++s) {
            prepare[ s ] = dp[ 0 ][ s ];
        }
    }

    return dp[ 0 ][ 0 ];
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < m; ++j) {
            scanf("%d", &grid[ i ][ j ]);
        }
    }

    printf("%d", compute());

    return 0;
}

3.2 题目2:贴瓷砖的方法数

3.2.1 问题描述

给定 \(n \times m\) 的空白区域,用无限多 \(1\times 2\) 规格的瓷砖铺满所有区域,返回合法的铺法数。

约束:\(1 \le n, m \le 11\)

3.2.2 轮廓线DP思路

  • 状态s表示轮廓线:s[0..j-1]位表示第i行左侧已处理列是否作为竖砖的上半部分,[j..m-1]位表示第i-1行右侧未处理列是否作为竖砖的上半部分

  • 决策类型:

    1. 上方已有竖砖的上半部分(sj位为 \(1\)):当前位置必须作为竖砖的下半部分,状态更新为 \(0\)
    2. 上方无竖砖(sj位为 \(0\)):
      • 竖放:需保证下方有行(\(i+1 \lt n\)),状态更新为 \(1\)
      • 横放:需保证右侧有列(\(j+1 \lt m\))且右侧无竖砖(sj+1位为 \(0\)),直接跳过右侧列

3.2.3 代码实现

版本1:轮廓线DP
// Problem: P10975 Mondriaan's Dream
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P10975
// Memory Limit: 512 MB
// Time Limit: 1000 ms

#include <cstdio>

typedef long long ll;

const int N = 15;

int n, m;
ll dp[ N ][ N ][ 1 << N ];

int get(int s, int j) {
    return (s >> j) & 1;
}

int set(int s, int j, int v) {
    return v == 0 ? (s & (~(1 << j))) : (s | (1 << j));
}

// 当前来到i行j列
// i-1行中,[j..m-1]列有没有作为竖砖的上点,状况用s[j..m-1]表示
// i行中,[0..j-1]列有没有作为竖砖的上点,状况用s[0..j-1]表示
// s表示轮廓线的状况
// 返回后续有几种摆放的方法
ll f(int i, int j, int s) {
    if (i == n) {
        return 1;
    }

    if (j == m) {
        return f(i + 1, 0, s);
    }

    if (dp[ i ][ j ][ s ] != -1) {
        return dp[ i ][ j ][ s ];
    }

    ll ans = 0;

    if (get(s, j) == 1) { // 上方没有竖着摆砖
        ans += f(i, j + 1, set(s, j, 0));
    } else {
        if (i + 1 < n) { // 当前竖着摆砖
            ans += f(i, j + 1, set(s, j, 1));
        }

        if (j + 1 < m && get(s, j + 1) == 0) { // 当前横着摆砖
            ans += f(i, j + 2, s);
        }
    }

    dp[ i ][ j ][ s ] = ans;

    return ans;
}

ll compute() {
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < m; ++j) {
            for (int s = 0; s < (1 << m); ++s) {
                dp[ i ][ j ][ s ] = -1;
            }
        }
    }

    return f(0, 0, 0);
}

int main() {
    while (true) {
        scanf("%d%d", &n, &m);
        if (n == 0 && m == 0) {
            break;
        }
        printf("%lld\n", compute());
    }

    return 0;
}
版本2:轮廓线DP+空间压缩
// Problem: P10975 Mondriaan's Dream
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P10975
// Memory Limit: 512 MB
// Time Limit: 1000 ms

#include <cstdio>

typedef long long ll;

const int N = 15;

int n, m;
ll dp[ N ][ 1 << N ];
ll prepare[ 1 << N ];

int get(int s, int j) {
    return (s >> j) & 1;
}

int set(int s, int j, int v) {
    return v == 0 ? (s & (~(1 << j))) : (s | (1 << j));
}

ll compute() {
    for (int s = 0; s < (1 << m); ++s) {
        prepare[ s ] = 1;
    }

    for (int i = n - 1; i >= 0; --i) {
		// j == m
        for (int s = 0; s < (1 << m); ++s) {
            dp[ m ][ s ] = prepare[ s ];
        }
		
		// 普通位置
        for (int j = m - 1; j >= 0; --j) {
            for (int s = 0; s < (1 << m); ++s) {
                ll ans = 0;
                if (get(s, j) == 1) {
                    ans += dp[ j + 1 ][ set(s, j, 0) ];
                } else {
                    if (i + 1 < n) {
                        ans += dp[ j + 1 ][ set(s, j, 1) ];
                    }

                    if (j + 1 < m && get(s, j + 1) == 0) {
                        ans += dp[ j + 2 ][ s ];
                    }
                }

                dp[ j ][ s ] = ans;
            }
        }
		
		// 设置prepare
        for (int s = 0; s < (1 << m); ++s) {
            prepare[ s ] = dp[ 0 ][ s ];
        }
    }

    return dp[ 0 ][ 0 ];
}

int main() {
    while (true) {
        scanf("%d%d", &n, &m);

        if (n == 0 && m == 0) {
            break;
        }

        printf("%lld\n", compute());
    }

    return 0;
}

3.3 题目3:相邻不同色的染色方法数

3.3.1 问题描述

给定 \(n \times m\)的区域和 \(k\) 种颜色(\(0\) ~ \(k-1\)),要求相邻格子(上、下、左、右)颜色不同。已知第 \(0\) 行和第 \(n-1\) 行的颜色,仅在 \(1\) ~ \(n-2\) 行染色,返回合法的染色方法数(对\(376544743\)取模)。

约束:

  • \(2 \le k \le 4\)
  • \(k = 2\) 时:\(1 \le n \le 1 \times 10^7\)\(1 \le m \le 1 \times 10^5\)
  • \(3 \le k \le 4\) 时:\(1 \le n \le 100\)\(1 \le m \le 8\)

3.3.2 轮廓线DP思路

  • 状态表示:由于颜色数最多 \(4\) 种,用 \(2\) 位二进制表示一个格子的颜色,状态s\(m \times 2\) 位的二进制数
  • 轮廓线定义:s[0..2j-1]位表示第i行左侧已处理列的颜色,[2j..2m-1]位表示第i-1行右侧未处理列的颜色
  • 决策约束:当前颜色需与左侧列和上方列颜色不同
  • 特殊处理:\(k=2\) 时存在奇偶性规律,可直接判断合法性(无需DP)

3.3.3 代码实现

版本1:轮廓线DP(会MLE)
// Problem: P2435 染色
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P2435
// Memory Limit: 125 MB
// Time Limit: 2000 ms

#include <cstdio>

const int LIMIT1 = 1e5 + 10, LIMIT2 = 1e2 + 10, LIMIT3 = 10;
const int mod = 376544743;

int n, m, k;
int start[LIMIT1], end[LIMIT1];
int startStatus, endStatus;
int dp[LIMIT2][LIMIT3][1 << (LIMIT3 << 1)];

int special() {
    if ((n & 1) == 0) {
        for (int i = 0; i < m; ++i) {
            if (start[i] == end[i]) {
                return 0;
            }
        }
    } else {
        for (int i = 0; i < m; ++i) {
            if (start[i] != end[i]) {
                return 0;
            }
        }
    }

    return 1;
}

// 在颜色状况s里,取出j号格的颜色
int get(int s, int j) { return (s >> (j << 1)) & 3; }

// 颜色状况s中,把j号格的颜色设置成v,然后把新的s返回
int set(int s, int j, int v) {
    return (s & (~(3 << (j << 1)))) | (v << (j << 1));
}

// 颜色状况a和颜色状况b,是否每一格都不同
bool different(int a, int b) {
    for (int j = 0; j < m; ++j) {
        if (get(a, j) == get(b, j)) {
            return false;
        }
    }
    return true;
}

// 当前来到i行j列
// i-1行中,[j..m-1]列的颜色状况,用s[j..m-1]号格子表示
// i行中,[0..j-1]列的颜色状况,用s[0..j-1]号格子表示
// s表示轮廓线的状况
// 返回有几种染色方法
int f(int i, int j, int s) {
    if (i == n - 1) {
        return different(s, endStatus);
    }

    if (j == m) {
        return f(i + 1, 0, s);
    }

    if (dp[i][j][s] != -1) {
        return dp[i][j][s];
    }

    int ans = 0;

    for (int color = 0; color < k; ++color) {
        if (((j == 0) || get(s, j - 1) != color) && (get(s, j) != color)) {
            ans = (ans + f(i, j + 1, set(s, j, color))) % mod;
        }
    }

    dp[i][j][s] = ans;

    return ans;
}

int compute() {
    startStatus = endStatus = 0;

    for (int j = 0; j < m; ++j) {
        startStatus = set(startStatus, j, start[j]);
        endStatus = set(endStatus, j, end[j]);
    }

    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < m; ++j) {
            for (int s = 0; s < (1 << (m << 1)); ++s) {
                dp[i][j][s] = -1;
            }
        }
    }

    return f(1, 0, startStatus);
}

int main() {
    scanf("%d%d%d", &n, &m, &k);

    for (int i = 0; i < m; ++i) {
        scanf("%d", &start[i]);
    }

    for (int i = 0; i < m; ++i) {
        scanf("%d", &end[i]);
    }

    if (k == 2) {
        printf("%d\n", special());
    } else {
        printf("%d\n", compute());
    }

    return 0;
}
版本2:轮廓线DP+空间压缩
// Problem: P2435 染色
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P2435
// Memory Limit: 125 MB
// Time Limit: 2000 ms

#include <cstdio>

const int LIMIT1 = 1e5 + 10, LIMIT2 = 10;
const int mod = 376544743;

int n, m, k;
int start[LIMIT1], end[LIMIT1];
int startStatus, endStatus;
int dp[LIMIT2][1 << (LIMIT2 << 1)];
int prepare[1 << (LIMIT2 << 1)];

int special() {
    if ((n & 1) == 0) {
        for (int i = 0; i < m; ++i) {
            if (start[i] == end[i]) {
                return 0;
            }
        }
    } else {
        for (int i = 0; i < m; ++i) {
            if (start[i] != end[i]) {
                return 0;
            }
        }
    }

    return 1;
}

int get(int s, int j) { return (s >> (j << 1)) & 3; }

int set(int s, int j, int v) {
    return (s & (~(3 << (j << 1)))) | (v << (j << 1));
}

bool different(int a, int b) {
    for (int j = 0; j < m; ++j) {
        if (get(a, j) == get(b, j)) {
            return false;
        }
    }
    return true;
}

int compute() {
    startStatus = endStatus = 0;

    for (int j = 0; j < m; ++j) {
        startStatus = set(startStatus, j, start[j]);
        endStatus = set(endStatus, j, end[j]);
    }

    for (int s = 0; s < (1 << (m << 1)); ++s) {
        prepare[s] = different(s, endStatus);
    }

    for (int i = n - 2; i >= 1; --i) {
		// j == m
        for (int s = 0; s < (1 << (m << 1)); s++) {
            dp[m][s] = prepare[s];
        }
		
		// 普通位置
        for (int j = m - 1; j >= 0; --j) {
            for (int s = 0; s < (1 << (m << 1)); ++s) {
                int ans = 0;

                for (int color = 0; color < k; ++color) {
                    if (((j == 0) || get(s, j - 1) != color) &&
                        (get(s, j) != color)) {
                        ans = (ans + dp[j + 1][set(s, j, color)]) % mod;
                    }
                }

                dp[j][s] = ans;
            }
        }
		
		// 设置prepare
        for (int s = 0; s < (1 << (m << 1)); s++) {
            prepare[s] = dp[0][s];
        }
    }
    return dp[0][startStatus];
}

int main() {
    scanf("%d%d%d", &n, &m, &k);

    for (int i = 0; i < m; ++i) {
        scanf("%d", &start[i]);
    }

    for (int i = 0; i < m; ++i) {
        scanf("%d", &end[i]);
    }

    if (k == 2) {
        printf("%d\n", special());
    } else {
        printf("%d\n", compute());
    }

    return 0;
}

3.4 题目4:摆放国王的方法数

3.4.1 问题描述

给定 \(n \times n\) 的区域,摆放 \(k\) 个国王,国王可攻击 \(8\) 个方向的相邻格子,要求国王之间不能互相攻击,返回合法的摆放方法数。

约束:\(1 \le n \le 9\)\(1 \le k \le n×n\)

3.4.2 轮廓线DP思路

  • 核心难点:国王的攻击范围包括 \(8\) 个方向,需额外考虑左上角\((i-1,j-1)\)的状态
  • 状态扩展:除轮廓线状态s外,新增参数leftup表示\((i-1,j-1)\)位置是否有国王
  • 决策约束:当前位置摆放国王的前提是左侧、左上角、上方、右上方均无国王
  • 状态维度:dp[i][j][s][leftup][k],其中k为剩余待摆放的国王数

3.4.3 代码实现

版本1:轮廓线DP
// Problem: P1896 [SCOI2005] 互不侵犯
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1896
// Memory Limit: 125 MB
// Time Limit: 1000 ms

#include <cstdio>

typedef long long ll;

const int N = 10, K = 90;

int n, kings;
ll dp[ N ][ N ][ 1 << N ][ 2 ][ K ];

int get(int s, int j) {
    return (s >> j) & 1;
}

int set(int s, int j, int v) {
    return v == 0 ? (s & (~(1 << j))) : (s | (1 << j));
}

// 当前来到i行j列
// i-1行中,[j..m-1]列有没有摆放国王,用s[j..m-1]号格子表示
// i行中,[0..j-1]列有没有摆放国王,用s[0..j-1]号格子表示
// s表示轮廓线的状况
// (i-1, j-1)位置,也就是左上角,有没有摆放国王,用leftup表示
// 国王还剩下k个需要去摆放
// 返回有多少种摆放方法
ll f(int i, int j, int s, int leftup, int k) {
    if (i == n) {
        return k == 0 ? 1 : 0;
    }

    if (j == n) {
        return f(i + 1, 0, s, 0, k);
    }

    if (dp[ i ][ j ][ s ][ leftup ][ k ] != -1) {
        return dp[ i ][ j ][ s ][ leftup ][ k ];
    }

    int left    = (j == 0) ? 0 : get(s, j - 1);
    int up      = get(s, j);
    int rightup = get(s, j + 1);

    ll ans = f(i, j + 1, set(s, j, 0), up, k);

    if (k > 0 && left == 0 && leftup == 0 && up == 0 && rightup == 0) {
        ans += f(i, j + 1, set(s, j, 1), up, k - 1);
    }

    dp[ i ][ j ][ s ][ leftup ][ k ] = ans;

    return ans;
}

ll compute() {
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            for (int s = 0; s < (1 << n); ++s) {
                for (int leftup = 0; leftup <= 1; ++leftup) {
                    for (int k = 0; k <= kings; ++k) {
                        dp[ i ][ j ][ s ][ leftup ][ k ] = -1;
                    }
                }
            }
        }
    }

    return f(0, 0, 0, 0, kings);
}

int main() {
    scanf("%d%d", &n, &kings);

    printf("%lld\n", compute());

    return 0;
}
版本2:轮廓线DP+空间压缩
// Problem: P1896 [SCOI2005] 互不侵犯
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1896
// Memory Limit: 125 MB
// Time Limit: 1000 ms

#include <cstdio>

typedef long long ll;

const int N = 10, K = 90;

int n, kings;
ll dp[N][1 << N][2][K];
ll prepare[1 << N][K];

int get(int s, int j) { return (s >> j) & 1; }

int set(int s, int j, int v) {
    return v == 0 ? (s & (~(1 << j))) : (s | (1 << j));
}

ll compute() {
    for (int s = 0; s < (1 << n); ++s) {
        for (int k = 0; k <= kings; ++k) {
            prepare[s][k] = (k == 0) ? 1 : 0;
        }
    }

    for (int i = n - 1; i >= 0; --i) {
		// j == n
        for (int s = 0; s < (1 << n); ++s) {
            for (int k = 0; k <= kings; ++k) {
                dp[n][s][0][k] = prepare[s][k];
                dp[n][s][1][k] = prepare[s][k];
            }
        }
		
		// 普通位置
        for (int j = n - 1; j >= 0; --j) {
            for (int s = 0; s < (1 << n); ++s) {
                for (int leftup = 0; leftup <= 1; ++leftup) {
                    for (int k = 0; k <= kings; ++k) {
                        int left = (j == 0) ? 0 : get(s, j - 1);
                        int up = get(s, j);
                        int rightup = get(s, j + 1);

                        ll ans = dp[j + 1][set(s, j, 0)][up][k];

                        if (k > 0 && left == 0 && leftup == 0 && up == 0 &&
                            rightup == 0) {
                            ans += dp[j + 1][set(s, j, 1)][up][k - 1];
                        }

                        dp[j][s][leftup][k] = ans;
                    }
                }
            }
			
			// 设置prepare
            for (int s = 0; s < (1 << n); ++s) {
                for (int k = 0; k <= kings; ++k) {
                    prepare[s][k] = dp[0][s][0][k];
                }
            }
        }
    }

    return dp[0][0][0][kings];
}

int main() {
    scanf("%d%d", &n, &kings);

    printf("%lld\n", compute());

    return 0;
}

4. 总结与思考

4.1 核心思想提炼

轮廓线DP的本质是状态的精准表示:通过轮廓线捕捉影响当前决策的最小必要信息,避免存储整行/整列的冗余状态,从而降低时间和空间复杂度。

关键步骤:

  1. 定义轮廓线:明确状态变量中每一位的含义(对应网格的哪些位置)
  2. 分析决策约束:当前位置的合法决策需满足哪些前置条件(如相邻位置状态)
  3. 状态转移:决策后如何更新轮廓线,确保后续决策能正确获取前置信息

4.2 空间压缩技巧

  1. 滚动数组:由于第i行的状态仅依赖第i+1行,可通过两个数组(当前行、下一行)交替存储
  2. 逆序遍历:从后往前处理列(j\(m-1\)\(0\)),复用同一数组存储不同列的状态
  3. 状态精简:根据问题特性减少状态维度(如题目4中leftup仅需0/1状态)

4.3 适用场景总结

轮廓线DP适用于以下问题:

  • 二维网格的逐行逐列决策问题
  • 相邻位置存在约束(如不能相邻、必须覆盖、颜色不同等)
  • 网格规模较小(\(m \le 12\),确保 \(2^m\) 状态量可控)

常见应用场景:

  • 放置类问题(种草、摆国王、摆棋子)
  • 覆盖类问题(贴瓷砖、多米诺骨牌覆盖)
  • 染色类问题(相邻不同色、特定颜色约束)

通过掌握轮廓线DP的核心思想和实现技巧,可高效解决各类二维网格约束优化问题,是算法竞赛中的重要进阶知识点。

posted @ 2026-01-17 22:58  Ty_66CCFf  阅读(1)  评论(0)    收藏  举报