状压 DP 做题记录
1.普通状态压缩 DP
I.P1896 [SCOI2005] 互不侵犯
\(f_{i,j,st}\) 表示前 \(i\) 行中放置了 \(j\) 个国王,当前行状态为 \(st\) 的方案数。可以预处理出合法的状态与其 popcount
,转移时枚举当前行状态和上一行状态,合法就转移。
const int N = 20,inf = 1e9,mod = 998244353;
const ll inff = 1e18;
int n,k,f[N][110][1000];
vector<int> v; int pcnt[1000];
il void solve() {
//------------code------------
read(n,k);
rep(st,0,(1 << n) - 1) if (!((st << 1) & st) && !((st >> 1) & st))
v.pb(st),pcnt[st] = __builtin_popcount(st);
f[0][0][0] = 1;
rep(i,1,n) for (auto st : v) for (auto stt : v)
if (!(stt & st) && !((stt << 1) & st) && !((stt >> 1) & st)) {
rep1(j,k,pcnt[st]) f[i][j][st] += f[i - 1][j - pcnt[st]][stt];
}
int ans = 0;
for (auto st : v) ans += f[n][k][st];
write(ans,'\n');
// cerr << "Time : " << (db)(end - start) / CLOCKS_PER_SEC << " s" << endl;
return ;
}
II.P2704 [NOI2001] 炮兵阵地
考虑将一行某个位置能否部署状压成一个二进制数,然后判断状态合法时可以直接把它们按位与起来。\(f_{i,st,st'}\) 表示前 \(i\) 行中,当前行状态为 \(st\),上一行状态为 \(st'\),最多能摆放的炮兵部队的数量。压掉一维就能过。
const int N = 110,inf = 1e9,mod = 998244353;
const ull base = 131,base_ = 233;
const ll inff = 1e18;
int n,m,a[N];
int f[3][1034][1034];
vector<PII> v;
il void solve() {
//------------code------------
read(n,m);
rep(i,1,n) {
string s; cin >> s;
rep(j,0,m - 1) a[i] += (s[j] == 'H') * (1 << (m - 1 - j));
}
rep(st,0,(1 << m) - 1) if (!(st & (st >> 1)) && !(st & (st >> 2))) v.pb(st,__builtin_popcount(st));
memset(f,-0x3f,sizeof f);
for (auto x : v) f[1][x.fst][0] = x.snd;
rep(i,2,n)
for (auto st : v) if (!(st.fst & a[i]))
for (auto stt : v) if (!(st.fst & stt.fst) && !(stt.fst & a[i - 1]))
for (auto sttt : v) if (!(st.fst & sttt.fst) && !(stt.fst & sttt.fst) && !(sttt.fst & a[i - 2]))
chmax(f[i % 3][st.fst][stt.fst],f[(i - 1) % 3][stt.fst][sttt.fst] + st.snd);
int ret = 0;
for (auto st : v) if (!(st.fst & a[n]))
for (auto stt : v) if (!(st.fst & stt.fst) && !(stt.fst & a[n - 1]))
chmax(ret,f[n % 3][st.fst][stt.fst]);
write(ret,'\n');
return ;
}
III.P1879 [USACO06NOV] Corn Fields G
板子。\(f_{i,st}\) 表示前 \(i\) 行中当前行状态为 \(st\) 的种植方案数。
const int N = 22,inf = 1e9,mod = 1e8;
const ull base = 131,base_ = 233;
const ll inff = 1e18;
int n,m,a[N]; ll f[N][4100];
vector<int> v;
il void solve() {
//------------code------------
read(n,m);
rep(i,1,n)
rep(j,1,m) {
int x; read(x);
a[i] += (1 << (m - j)) * (!x);
}
rep(st,0,(1 << m) - 1) if (!(st & (st << 1))) v.pb(st);
f[0][0] = 1;
rep(i,1,n)
for (auto st : v) if (!(st & a[i]))
for (auto stt : v) if (!(st & stt) && !(stt & a[i - 1]))
f[i][st] = (f[i][st] + f[i - 1][stt]) % mod;
ll ans = 0;
for (auto st : v) ans = (ans + f[n][st]) % mod;
write(ans,'\n');
return ;
}
IV.P3052 [USACO12MAR] Cows in a Skyscraper G
\(f_{st}\) 表示当前已将哪些物品进行分组,最小的分组数量。维护一个当前 \(f_{st}\) 的最大剩余体积,每次能放下就放,放不下就增加组数,同时更新最大剩余体积。
const int N = 3e5 + 10,inf = 1e9,mod = 998244353;
const ull base = 131,base_ = 233;
const ll inff = 1e18;
int n,m,w[20]; PII f[N];
il void solve() {
//------------code------------
read(n,m);
rep(i,1,n) read(w[i]);
rep(st,1,(1 << n) - 1) f[st] = {-inf,inf};
f[0] = {m,1};
rep(st,1,(1 << n) - 1)
rep(i,0,n - 1) if (st >> i & 1) {
if (f[st].snd >= f[st ^ (1 << i)].snd && f[st ^ (1 << i)].fst >= w[i + 1]) {
chmax(f[st].fst,f[st ^ (1 << i)].fst - w[i + 1]);
f[st].snd = f[st ^ (1 << i)].snd;
} else if (f[st].snd >= f[st ^ (1 << i)].snd + 1 && f[st ^ (1 << i)].fst < w[i + 1]) {
chmax(f[st].fst,m - w[i + 1]);
f[st].snd = f[st ^ (1 << i)].snd + 1;
}
// cerr << st << " " << i << " " << f[st].snd << '\n';
}
write(f[(1 << n) - 1].snd,'\n');
return ;
}
V.P2396 yyy loves Maths VII
显然有 \(\mathcal{O(2^n n)}\) 的做法,但是 1 s
可能跑不过,考虑优化 \(n\) 的那一维,每次取出状态的 lowbit
来进行转移,并减掉它,这样可以减少大量无用的判断,能过此题。最慢的点跑了有 600 ms
。
const int N = 2e7 + 10,inf = 1e9,mod = 1e9 + 7;
const ull base = 131,base_ = 233;
const ll inff = 1e18;
int n,m,a[N],b[3],f[N],g[N];
il int lowbit(int x) { return x & -x; }
il void solve() {
//------------code------------
read(n); rep(i,1,n) read(a[(1 << i - 1)]);
read(m); rep(i,1,m) read(b[i]);
f[0] = 1;
rep(st,1,(1 << n) - 1) {
g[st] = g[st ^ lowbit(st)] + a[lowbit(st)];
bool fl = 1;
rep(i,1,m) if (g[st] == b[i]) { fl = 0; break; }
if (!fl) continue;
int x = st;
for (; x; x -= x & -x) f[st] = (f[st] + f[st ^ lowbit(x)]) % mod;
}
write(f[(1 << n) - 1],'\n');
return ;
}
VI.P2831 [NOIP2016 提高组] 愤怒的小鸟
可以发现两个点可以唯一确定一条抛物线,预处理 \(sta_{i,j}\) 表示第 \(i\) 个点和第 \(j\) 个点组成的抛物线经过的点,对其做状压处理。现在就有一个 \(\mathcal{O(T2^n n^2)}\) 的做法,对于当前状态枚举接下来打哪条抛物线。这样并不能通过所有测试点,考虑优化。发现最终需要的答案是 \(f_{2^n - 1}\),其余有 \(f\) 对最终答案并没有贡献,那么它是无用的,显然我们最终状态 \(2^n - 1\) 在二进制下是有 \(n\) 个 1
的,那么每次对于当前状态要推到最终状态,二进制下第一个为 0
的位置到最后一定会被转移掉,变为 1
,那么我们就可以找到这个位置,再枚举它和其它的点的抛物线,用此抛物线的 \(sta\) 来转移就足矣。最后时间复杂度是 \(\mathcal{O(T2^n n)}\) 的。
const int N = 270010,inf = 1e9,mod = 998244353;
const ull base = 131,base_ = 233;
const db eps = 1e-8;
const ll inff = 1e18;
int n,m; pair<db,db> a[N];
int cnt[20][20],f[N];
il void solve() {
//------------code------------
read(n,m);
rep(i,1,n) cin >> a[i].fst >> a[i].snd;
rep(i,1,n) rep(j,i + 1,n) {
db x = a[i].fst,y = a[i].snd;
db xx = a[j].fst,yy = a[j].snd;
if (fabs(x - xx) < eps) { cnt[i][j] = cnt[j][i] = 0; continue; }
db A = (y / x - yy / xx) / (x - xx),B = y / x - x * A;
// cerr << i << " " << j << " " << A << " " << B << '\n';
if (A >= 0) { cnt[i][j] = cnt[j][i] = 0; continue; }
cnt[i][j] = 0;
rep(k,1,n) if (fabs(A * a[k].fst * a[k].fst + B * a[k].fst - a[k].snd) < eps)
cnt[i][j] += (1 << k - 1);
cnt[j][i] = cnt[i][j];
}
rep(st,0,(1 << n) - 1) f[st] = inf;
f[0] = 0;
rep(st,0,(1 << n) - 2) {
int p = -1;
rep(i,0,n - 1) if (!(st >> i & 1)) { p = i; break; }
chmin(f[st | (1 << p)],f[st] + 1);
rep(i,1,n) if (i != p && ~cnt[p + 1][i]) chmin(f[st | cnt[p + 1][i]],f[st] + 1);
}
write(f[(1 << n) - 1],'\n');
return ;
}
VII.P2473 [SCOI2008] 奖励关
显然有 \(f_{i,st}\) 表示前 \(i\) 次中,吃过的宝物的集合为 \(st\) 的最大期望分值。然后你会发现对于 \(f_i\),有一些状态是明显不合法的,此题起点唯一,但终点不唯一且有一些状态不可达的情况,所以需要倒推。
const int N = 20,inf = 1e9,mod = 998244353;
const ull base = 131,base_ = 233;
const ll inff = 1e18;
int k,n,s[N],p[N];
db f[110][32770];
il void solve() {
//------------code------------
read(k,n);
rep(i,1,n) {
read(p[i]); s[i] = 0;
int x; read(x);
while (x) {
s[i] |= (1 << x - 1);
read(x);
}
}
f[k + 1][(1 << n) - 1] = 0;
rep1(i,k,1)
rep(st,0,(1 << n) - 1) {
// cerr << i << " " << st << '\n';
rep(j,1,n)
if ((st & s[j]) == s[j]) f[i][st] += max((f[i + 1][st | (1 << j - 1)] + p[j] * 1.0),f[i + 1][st]) / (db)n;
else f[i][st] += f[i + 1][st] / (db)n;
}
db ans = f[1][0];
printf("%.6lf\n",ans);
return ;
}
2.插头 DP
插头 DP 用于解决需要维护状态连通性的状压 DP 问题。
I.「HDU 1400」Mondriaan’s Dream
\(f_{i,j,s}\) 表示当前转移到了 \((i,j)\),轮廓线上的状态为 \(s\) 的合法覆盖方案数。注意状态是按照 \(j\) 的大小从小到大依次存储在二进制位下的。
可以进行分类讨论:
- 当状态 \(s\) 的第 \(j - 1\) 位上为
1
而第 \(j\) 位上为0
,即左边有插头过来而上面没有,那么就是横放,下一个状态的第 \(j - 1\) 位应当为0
; - 当状态 \(s\) 的第 \(j\) 位上为
1
而第 \(j - 1\) 位上为0
,即上面有插头过来而左边没有,那么就是竖放,下一个状态的第 \(j\) 位应当为0
; - 当第 \(j\) 和 \(j - 1\) 位上均为
0
时,即可竖放也可横放,结合以上两种转移即可; - 均为
1
时不合法。
const int N = 15,inf = 1e9,mod = 998244353;
const ull base = 131,base_ = 233;
const ll inff = 1e18;
int n,m,f[N][N][1 << N];
il void solve() {
//------------code------------
while (1 + 1 == 2) {
read(n,m);
if (!n && !m) return ;
rep(i,0,n) rep(j,0,m) rep(s,0,(1 << m + 1) - 1) f[i][j][s] = 0;
f[1][0][0] = 1;
rep(i,1,n) {
rep(j,1,m) {
rep(s,0,(1 << m + 1) - 1) {
if (!(s >> (j - 1) & 1)) f[i][j][s ^ (1 << j)] += f[i][j - 1][s];
if (!(s >> j & 1)) f[i][j][s ^ (1 << j - 1)] += f[i][j - 1][s];
}
}
rep(s,0,(1 << m + 1) - 1) if (!(s >> m)) f[i + 1][0][s << 1] = f[i][m][s];
}
write(f[n + 1][0][0],'\n');
}
return ;
}
「UVA 11270」Tiling Dominoes
双倍经验。同上。
等待更新。。