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\) 串的最小步数。

分类讨论,进行下一步转化:

  1. 将一对 \(0\) 取反,显然会使答案更劣,不可能发生。
  2. 将一个 \(0\) 一个 \(1\) 取反,可看做原有的 \(1\) 移动至 \(0\) 的位置。
  3. 将一对 \(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;
}
posted @ 2020-07-13 08:21  Luckyblock  阅读(147)  评论(0编辑  收藏  举报