CF79D Password
知识点: DP,差分,Bfs
双倍经验 P3943 星空。
将此题代码交过去可直接 AC,但 P3943 数据较弱,没有卡掉错误的背包解法。
完全背包解法错误原因 详见 题解 P3943 【星空】 - Epworth 的博客。
简述
给定一长度为 \(n\) 的 \(0\) 串,给定 \(k\) 个 位置。
给出 \(m\) 个长度 \(b_i\),每次可选择一段长度为 \(b_i\) 的子序列取反。
求使 \(k\) 个位置变为 \(1\) ,其他位置仍为 \(0\) 的最小步数。
\(1\le n\le 10^4,\ 1\le m\le 100,\ 1\le b_i\le n,\ k\le 10\)。
1S,256MB。
分析
发现题目等价于 一开始只有 \(k\) 个位置开着灯,使所有灯都关上的最小步数。
设关灯为 \(0\),开灯为 \(1\),以下均按照上述等价情况展开。
看到区间修改,考虑差分。但此题为区间取反,一般的作差差分无法使用,考虑异或差分。
令 \(b_i = a_i\ \text{xor}\ a_{i+1}\),对于样例 1,有:
差分前 | \(\ \ 1 1 1 0 1 1 1 1 1 0\) |
---|---|
差分后 | \(10011000010\) |
在差分后数组前添加一个 \(0\) 位置,手玩后发现,差分后必然有偶数个 \(1\) 出现。
此时若进行区间取反,只会使区间两端位置改变,中间不变。若将原序列中 \(1\sim 3\) 取反,则有:
差分前 | \(\ \ 0 0 0 0 1 1 1 1 1 0\) |
---|---|
差分后 | \(00001000010\) |
发现只有 \(0,3\) 两个位置的 \(1\) 被改变,则问题可以转化为:
给定一有 \(2k\) 个位置为 \(1\) 的 01 串。
每次可选择一对距离为 \(b_i\) 的位置,将其取反。
求将其变为 \(0\) 串的最小步数。
分类讨论,进行下一步转化:
- 将一对 \(0\) 取反,显然会使答案更劣,不可能发生。
- 将一个 \(0\) 一个 \(1\) 取反,可看做原有的 \(1\) 移动至 \(0\) 的位置。
- 将一对 \(1\) 取反,可看做一个 \(1\) 移动到另一个的位置,两个 \(1\) 碰撞变成 \(0\)。
一定会发生偶数次 两个 \(1\) 碰撞的情况。考虑每次使两个 \(1\) 碰撞的最小代价。
若已知两个 \(1\) 的位置为 \(u,v\),使其碰撞的代价即 使用 \(b_i\) 和 \(-b_i\) 凑出 \(v - u\) 的步数。显然可以 bfs 预处理出碰撞每对 \(1\) 的花费。
问题转化为:
给定 \(2k\) 个物品,每次可取出一对物品。
取出每对物品的花费已知,求全取出的最小花费。
由于 \(k\le 10\),显然可以压缩 \(k\) 个物品选/不选的状态,接下来就是个很简单的状压 DP 了。每次枚举两个不在集合中的物品 加入集合转移即可。
代码
//知识点:DP,差分,Bfs
/*
By:Luckyblock
异或差分 + Bfs + 状压DP
*/
#include <queue>
#include <cstdio>
#include <ctype.h>
#include <cstring>
#include <algorithm>
#define ll long long
const int kMaxm = 110;
const int kMaxn = 1e4 + 10;
const int kInf = 1e9 + 2077;
//=============================================================
int n, k, m, cnt;
int pos[kMaxm], a[kMaxn], b[kMaxm];
int dis[kMaxn], map[kMaxn], cost[30][30];
int f[(1 << 21) + 10];
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void GetMin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
void Bfs(int s) { //预处理出碰撞每对 1 的花费。
std :: queue <int> q;
memset(dis, 63, sizeof (dis));
dis[s] = 0;
q.push(s);
while (! q.empty()) {
int u = q.front(); q.pop();
for (int i = 1; i <= m; ++ i) {
int v1 = u + b[i], v2 = u - b[i]; //v1 向右扩张,v2 向左。
if (v1 <= n && dis[v1] > kInf) dis[v1] = dis[u] + 1, q.push(v1);
if (v2 >= 1 && dis[v2] > kInf) dis[v2] = dis[u] + 1, q.push(v2);
}
}
for (int i = 1; i <= n; ++ i) {
if (map[i]) cost[map[s]][map[i]] = dis[i];
}
}
void Prepare() {
n = read(), k = read(), m = read();
for (int i = 1; i <= k; ++ i) a[read()] = 1;
for (int i = 0; i <= n; ++ i) a[i] ^= a[i + 1];
for (int i = 1; i <= m; ++ i) b[i] = read();
for (int i = 0; i <= n; ++ i) {
if (a[i]) map[i] = ++ cnt; //建立映射关系
}
for (int i = 0; i <= n; ++ i ) if (a[i]) Bfs(i);
}
//=============================================================
int main() {
Prepare();
memset(f, 63, sizeof (f)); f[0] = 0;
int all = (1 << cnt) - 1;
for (int i = 0; i < all; ++ i) {
if (f[i] > kInf) continue ;
for (int x = 1; x <= cnt; ++ x) {
if ((1 << (x - 1)) & i) continue ;
for (int y = x + 1; y <= cnt; ++ y) { //每次枚举两个 1 转移
if ((1 << (y - 1)) & i) continue ;
GetMin(f[i | (1 << (x - 1)) | (1 << (y - 1))],
f[i] + cost[x][y]);
}
}
}
printf("%d", f[all] < kInf ? f[all] : - 1);
return 0;
}