CTT 2023 D3T3 黄焖鸡 题解
link:qoj 10306
- 定义正整数序列 \(a\) 的最大权独立集 \(W(a)\) 为:从 \(a\) 中选择若干不相邻的数,选出的数的和的最大值。
若 \(2W(a)\,{\color{red}\neq} \sum a_i\) 就说 \(a\) 是好的,否则就是坏的。
若正整数序列 \(a\) 的所有子区间都是好的,就说 \(a\) 是极好的。
给定 \(n,m\) 和一个 \(n\times m\) 的 \(01\) 表格 \(v\),其中 \(v_{i,j}\) 表示 \(a_i\) 能否填 \(j\),并且只能 \(a_i\in [1,m]\)。
求所有合法填 \(a\) 方案中,有多少能使得 \(a\) 是极好的,对 \(998244353\) 取模。
\(1\le n\le 200,1\le m\le 19\)。部分分:\(\color{red}{m\le 16}\)。
猜结论,猜如猜,还是得有顺推做法。
顺推做法
考虑把做最大独立集的过程搞个类似 ddp 的状态。
怎么判定呢?维护 \(A/B,s\) 分别表示:最后一个元素爱选不选/一定不选,所有元素的和。
从前往后扫描,更新 \(A,B,s\),如果某时刻 \(2A=s\) 就变坏。
什么时候会出现 \(2A=s\) 的情况?
如果一组 \((A,B,s)\) 加入 \(x\) 后变坏了,即 \(2\max(A,B+x)=s+x\)。
-
若 \(x\ge A-B\),就是 \(2B+2x=s+x\),也即 \(x=s-2B\)。
-
若 \(x\le A-B\),就是 \(2A=s+x\),也即 \(x=2A-s\)。
于是我们不维护 \(A,B\),转而维护 \(u=2A-s,v=s-2B\)。
条件变为:
加入 \(x\) 后,\(s\gets s+x,A\gets \max(A,B+x),B\gets A\),则容易推出 \((u,v)\) 的变化为:
由组合意义天然有:\(u\ge v\),并且一旦 \(u>v\) 则显然会一直保持。但是我们又要 \(u\le v\) 才有限制。
于是我们只需记录所有后缀 \(u=v\) 的状态即可。下面会具体讲。
若加入 \(x<u\),\((u,u)\xrightarrow{\text{add }x} (u-x,x-u)\),此时 \(u-x>0>x-u\),于是之后序列一定是好的。
若加入 \(x=u\),则由于 \(x\neq u\),于是序列变坏。
于是只能加入 \(x>u\) 的数,\((u,u)\xrightarrow{\text{add }x} (x-u,x-u)\)。
然后就能设计 dp。记 \(f_{i,S}\) 表示考虑 \(a_1\sim a_i\),后缀 \((u,u)\) 状态中 \(u\) 形成的集合为 \(S\) 的方案数。
转移就是枚举第 \(i\) 位可填的 \(x\),枚举 \(x\not\in S\) 的状态 \(S\),然后转移到 \(S'=\{x\}\cup\{x-u:u\in S,u<x\}\) 即可。
此时称 \(S'=to(S,x)\)。
猜结论做法
结论:考虑交替和 \(s_i=a_i-s_{i-1}\),\(a\) 是坏的当且仅当 \(\forall i,s_i\ge 0,s_n=0\)。
然后就 dp,设 \(f_{i,S}\) 表示考虑 \(a_1\sim a_i\),后缀中出现的 \(s\) 的集合为 \(S\) 的方案数。
转移也是一样转移,两个 dp 是完全一致的。
如何证明结论呢?
对于一个选取状态,下面记 \(1\) 表示某个位置取了,\(0\) 表示某个位置不取。
正推
你就取 $1010\cdots $ 和 \(0101\cdots\) 这两个状态。
由于 \(2W(a)=\sum a_i\),于是只能奇数位和与偶数位和都是总和的一半。此时自然就 \(s_n=0\)。
并且如果 \(s_i<0\),就意味着 \([\cdots,-1,1,-1,1]\) 这样一个带权和 \(<0\)。
于是保留同奇偶的状态 \(>i\) 的部分,让状态 \(\le i\) 部分 \(01\) 反转,就存在一个独立集和比一半大,矛盾了。
于是只能 \(\forall i,s_i\ge 0\)。
反推
- 用调整法把一个 \(01\) 状态一直加非负数调整为 \(01\) 交替的串,变成最大独立集。
考虑在一个 \(01\) 状态下,考虑 \(+s_i\) 带来的影响:
当前缀状态为 \(\cdots 1010\) 的时候,权值 \(+s_i\),则状态会变成 \(\cdots 0101\)。
考虑第一次连续出现两个 \(0\) 的位置,设此时的为 ${\color{red}{\cdots01010}}0\cdots $。
你操作红色部分,变成 ${\color{red}{\cdots10101}}0\cdots $,然后一直调整一直调整即可。
下文令 \(m\gets m+1\)。
你现在有一个 dp 转移:\(f(i-1,S)\xrightarrow{v(i,x)=1} f(i,to(S,x))\)。具体的:
if(v[i][x]&&(j>>x&1^1)) ad(f[i][1<<x|(R[j]>>(m-x))],f[i-1][j]);
if(v[i][x]&&(j>>x&1^1)) ad(f[i][R[j+1]>>(m-x)],f[i-1][j]);//一个东西,和上面等价
其中 \(R[j]\) 表示 \(j\) 的每个二进制位 \(b\to m-b\) 之后的状态,就是 reverse 的意思。写成下面那样好看一点,是等价的。
你直接做是枚举 \(i,S,x\),复杂度 \(\mathcal{O}(nm2^m)\),常数小能通过 \(m\le 16\) 的部分分。
考虑 R[j+1]>>(m-x) 相同的等价类长啥样,发现应该是 \(j\) 的 \([1,m-x)\) 位可以任意变,其他不能变。
你做个超集和状物就行了,每次的枚举量从 \(2^m\) 变为的等价类大小 \(2^{m-x}\) 级别。
于是总枚举量从 \(\mathcal{O}(m2^m)\to \sum\limits_{x=1}^{m-1}2^{m-x}=\mathcal{O}(2^m)\)。复杂度也就是正确的 \(\mathcal{O}(n2^m)\) 了,可以通过。
代码实在短,并且很能辅助理解,我就放了。
#include<bits/stdc++.h>
#define LL long long
#define fr(x) freopen(#x".in","r",stdin);freopen(#x".out","w",stdout);
using namespace std;
const int N=1<<20,mod=998244353;
int n,m,a[21],f[N],F[N],R[N];
inline void ad(int &x,int y){x+=y;(x>=mod)&&(x-=mod);}
int main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);cin>>n>>m;
int u=1<<(++m);f[0]=1;
for(int i=0;i<=u;i++) for(int x=i;x;x&=(x-1)) R[i]|=1<<(m-__builtin_ctz(x));
for(int i=1;i<=n;i++)
{
for(int j=1;j<m;j++) cin>>a[j];fill_n(F,u,0);
for(int j=m-1,p=u/2;j;j--,p>>=1)
{
if(a[j]) for(int s=0;s<p;s+=2) ad(F[R[s+1]>>(m-j)],f[s]);
for(int s=p;s<p*2;s+=2) ad(f[s^p],f[s]);
}memcpy(f,F,u<<2);
}int s=0;
for(int i=0;i<u;i+=2) ad(s,f[i]);
return cout<<s,0;
}
总结
顺推做法算是在做 ddp 状物,只是要探究很多性质。
猜结论做法的反推很自然,但是正推比较不自然。只能说我见过好几个调整法调整前缀的题了,可以积累思路。
最后 dp 的转移优化大概还是:相同的转移合并一起做,减少重复。

浙公网安备 33010602011771号