Loading

状态压缩 DP

状态压缩 DP

这种 DP 没有什么特殊的地方,在考虑问题的时候仍然像以前一样去考虑,但是当你注意到题中有一个特别小的数字的时候,或者你发现不能很好地设计状态的时候,你就可以考虑状态压缩。当然有时候可以考虑直接搜索后优化。

所谓状态压缩,就是将 DP 状态全部记录下来,一般采用二进制,即每一位都是一个独立的状态,代表某个东西的有无之类。

【引入】P1879 [USACO06NOV]Corn Fields G:题意如下。

农场主 $\rm John$ 新买了一块长方形的新牧场,这块牧场被划分成 $M$ 行 $N$ 列 $(1 \le M \le 12, 1 \le N \le 12)$,每一格都是一块正方形的土地。 $\rm John$ 打算在牧场上的某几格里种上美味的草,供他的奶牛们享用。

遗憾的是,有些土地相当贫瘠,不能用来种草。并且,奶牛们喜欢独占一块草地的感觉,于是 $\rm John$ 不会选择两块相邻的土地,也就是说,没有哪两块草地有公共边。

$\rm John$ 想知道,如果不考虑草地的总块数,那么,一共有多少种种植方案可供他选择?(当然,把新牧场完全荒废也是一种方案)

观察一下,数据范围非常小,而我们假如把行作为阶段,想从上一行转移到这一行来,势必要知道一行中所有的位置的情况。而我们就可以使用状压来表示每一行中草地的选取情况。不妨设二进制从低到高第 $j$ 位就表示这一行草地的第 $j$ 块有没有被选取。

于是我们可以设计出状态:$f_{i,S}$ 表示第 $i$ 行状态为 $S$ 时的方案数。转移方程也很显然:$f_{i,S} \leftarrow f_{i-1,S'}$ 其中满足 $S,S'$ 均合法,并且 $S,S'$ 的上下组合也是合法的。

我们不妨来计算一下这样写的复杂度,为 $\mathcal{O}(n\times 2^{2m})$,我没有试过直接写能不能过,但是显然的,我们有一个剪枝优化。

注意到我们每次都在判断是否可行,尤其是行内的判断,浪费了我们很多时间,而这些行的情况已经给出,我们为什么不预处理呢?具体的,我们首先暴力枚举每一行的每一个情况,把合法的存下来,转移的时候只枚举合法的,这样只需要一次判断(上下行)。

参考代码:

int n,m,a[N][N],f[N][M];
vector<int> s[N];
int ck(int x){
    int lst = 0;
    while(x){
        if(lst&&(x&1)) return 0;
        lst = x&1; x>>=1;
    }return 1;
}
int ck(int x,int y){
    F(i,1,m){
        if(x%2==y%2&&x%2==1) return 0;
        x/=2; y/=2;
    }return 1;
}
void solve(){
    n = rd();m = rd();
    F(i,1,n){
        int st = 0;
        F(j,1,m) st = st * 2 + !rd();
        for(int j=0;j<(1<<m);j++){
            if((j&st)!=0||!ck(j))continue;
            s[i].push_back(j);
        }
    }
    for(int j:s[1]) f[1][j] = 1;
    F(i,2,n)
        for(int j:s[i])
            for(int k:s[i-1])
                if(ck(j,k))
                    f[i][j]+=f[i-1][k];
    int ans = 0;
    for(int j:s[n]) ans=(ans+f[n][j])%mod;
    cout<<ans<<endl;
}

当然,我写的代码还算是比较直观的,也偏长一点,对于位运算运用得当的高手而言,代码是很简单的。

我们再看两个非常经典的题。

【例题 1】P2704 [NOI2001] 炮兵阵地:

一个 $N\times M$ 的地图由 $N$ 行 $M$ 列组成,地图的每一格可能是山地(用 $\texttt{H}$ 表示),也可能是平原(用 $\texttt{P}$ 表示),如下图。$\texttt{P}$ 的位置可以放兵,攻击范围如图所示,不受地形影响。

求不误伤队友的前提下能放的兵最多有几个。
$1\le N\le 100,1\le M\le 10$

考虑状压。因为炮兵的攻击范围有点大,想当然我们的 DP 状态也需要多记录几行。

总体思路是一样的:

  1. 处理出行内的合法情况。
  2. 判断不同行之间的情况是否合法。
  3. 根据朴素的动态规划进行转移。

【例题 2】P1896 [SCOI2005] 互不侵犯:
和上一题几乎一样,只是把攻击范围稍加调整,思路不变。建议读者自行思考。

上面的几题都是裸的状压 DP,下面介绍几个经典的应用。

【例题 3】P3092 [USACO13NOV] No Change G:

约翰到商场购物,他的钱包里有 $(1 \le K \le 16)$ 个硬币,他想按顺序买 $N$ 个物品 $(1 \le N \le 10^5)$,第 $i$ 个物品需要花费 $(1 \le c_i \le 10^4)$ 块钱。

在依次进行的购买N个物品的过程中,约翰可以随时停下来付款,每次付款只用一个硬币,支付购买的内容是从上一次支付后开始到现在的这些所有物品(前提是该硬币足以支付这些物品的费用)。不幸的是,商场的收银机坏了,如果约翰支付的硬币面值大于所需的费用,他不会得到任何找零。

请计算出在购买完 $N$ 个物品后,约翰最多剩下多少钱。如果无法完成购买,输出 $-1$。

$K$ 很小,容易想到要状压,但是我们要记录的是什么?可行性吗?未免有点浪费,考虑把 $f_S$ 记为在硬币使用情况为 $S$ 时最多能买到哪个商品,当 $f_S=N$ 时我们可以快速统计出答案,这样也避免了枚举 $N$ 的限制,现在问题抛给转移。

容易看出 $f_S$ 是具有单调性的,假如你能买到第 $x$ 个物品,显然买 $x-1$ 个不成问题,假如连 第 $x$ 个物品都买不到,更不要说 $x+1$ 个了。所以我们二分这个最大位置。

配合前缀和实现 $\mathcal{O}(\log n)$ 的转移。

【例题 4】CF1550E Stringforces:

设 $s$ 是一个由前 $k$ 个小写字母构成的字符串,$v$ 是前 $k$ 个小写字母中的某一个。定义 $\mathrm{MaxLen}(s,v)$ 表示 $s$ 所有仅由字母 $v$ 构成的连续子串的最长长度。
定义 $s$ 的价值为所有 $\mathrm{MaxLen}(S,v)$ 的最小值,其中 $v$ 取遍前 $k$ 个小写字母。
现在给定一个长度为 $n$ 的字符串 $s$,$s$ 中字母要么是前 $k$ 个小写字母中的某一个,要么是问号。你需要将 $s$ 中的每一个问号替换成前 $k$ 个小写字母中的一个,并最大化 $s$ 的价值。方便起见,你只需要输出这个最大的价值即可。
保证 $1\leq n\leq 2\times 10^5$,$1\leq k\leq 17$。

$K$ 很小,而且你发现 $17$ 这个数字显然是选过的,如果他想考你 $\mathcal{O}(26n)$ 的字符串 DP,为啥要修改字符集呢?而 $2^{17}=131072$,基本与 $n$ 同阶,$k!$ 又远远不足以通过此题。

进一步分析题目,发现题目要求这个最小值最大是多少,套路地二分答案。

我们考虑当二分出的答案为 $x$,就变成判定性问题,同上,我们假如记录可行性未免浪费,不妨设一个相似的状态,$f_S$ 表示在 $S$ 的状态下,能够满足条件达到的最小前缀,显然前缀越小,留给后面的空间就越大,这里是一个类似贪心的思想。

通过预处理每个点后的信息,也就是下一个满足条件的前缀位置,进行转移即可。

暂时就到这里,因为笔者做的题太少,以后遇到好题了可以补充进来。

posted @ 2024-01-18 20:15  紊莫  阅读(23)  评论(0)    收藏  举报  来源