「Codeforces 79D」Password


Description

有一个 01 序列 \(a_1,a_2,\cdots,a_n\),初始时全为 \(0\)

给定 \(m\) 个长度,分别为 \(l_1\sim l_m\)

每次可以选择一个长度为某个 \(l_i\) 区间,对其进行翻转操作(\(0\to 1,1\to 0\))。

求最少的操作次数,使得最后有且仅有 \(k\) 个位置为 \(1\)\(k\) 个位置给定),其余为 \(0\)

\(1\leq n\leq 10^4,1\leq k\leq 10,1\leq m\leq 100\)

Solution

原问题等价于:

给定 01 序列 \(a_1,a_2,\cdots,a_n\),有 \(k\) 个位置为 \(1\),其余为 \(0\)。每次可以翻转长度为 \(l_i\) 的区间,求将 \(a\) 清零的最小操作数。

操作为区间修改,考虑差分。

由于是“区间取反”,一般的作差差分无法使用,考虑 异或差分。令 \(b_i=a_i\text{ xor }a_{i+1}\)(设 \(a_0=a_{n+1}=0\))。

那么,将原序列中的区间 \([l,r]\) 翻转,等价于将差分序列中的 \(b_{l-1},b_r\) 取反(其他元素不变)。

Step1

考虑到 \(b\) 序列初始最多只有 \(2k\)\(1\),则问题转化为:

给定 01 序列 \(b_0,b_2,\cdots,b_n\),最多有 \(2k\) 个位置为 \(1\)。每次可以选择一对距离为 \(l_i\) 的位置,将其取反。求将 \(b\) 清零的最小操作次数。

设选择的一对位置为 \((x,y)\)分类讨论:

  • \(b_x=0,b_y=0\),则操作后 \(b_x=1,b_y=1\),增加 \(2\)\(1\)。(显然会使答案更劣,不会发生)

  • \(b_x=1,b_y=1\),则操作后 \(b_x=0,b_y=0\),相当于 \(2\)\(1\) 碰撞变成 \(0\),减少 \(2\)\(1\)

  • \(b_x=1,b_y=0\),则操作后 \(b_x=0,b_y=1\),相当于把 \(x\) 上的 \(1\) 移到 \(y\)\(1\) 的数量不变。

  • \(b_x=0,b_y=1\),与 \(b_x=1,b_y=0\) 同理,\(1\) 的数量不变。

Step2

问题等价于:(第 \(i\) 个节点有标记相当于 \(b_i=1\)

给定一个有 \(n+1\) 个节点的图(点的编号为 \(0\sim n\))。当 \(dis(x,y)=l_i\) 时,存在边 \((x,y)\)。初始时最多有 \(2k\) 个节点上有标记,每次可以沿边移动标记。两个标记相遇就会消失。求使所有标记消失的最少移动次数。

设标记点分别为 \(p_0,p_1,\cdots,p_{g-1}\)

首先,我们可以通过 BFS 计算出所有标记点对之间的距离。

\(2k\leq 20\),考虑 状压 DP(差分序列中为 \(0\) 的位置不用管,只考虑 \(2k\)\(1\),有 \(2^{2k}\) 种状态)。令 \(f_S\) 表示标记点状态为 \(S\) 时使所有标记消失的最少移动次数。

\(S\) 二进制下的第 \(i\) 位为 \(1\) 表示标记点 \(p_i\) 上的标记未消失。显然转移只需考虑 \(2\)\(1\) 碰撞变成 \(0\) 的情况,其他情况都是没有意义的,所以我们不需要考虑非初始标记点的状态)

转移:\(S\) 二进制下为 \(1\) 的其中两个位为 \(i,j\)\(f_{S}=\min\{f_{S-2^i-2^j}+dis(i,j)\}\)

初始时 \(f_0=0,f_i=\infty\,(i\neq 0)\)。答案即为 \(f_{2^g-1}\)

#include
#define int long long
using namespace std;
const int N=1e4+5,M=22;
int n,m,k,x,a[N],b[N],l[N],g,c[N],d[N],dis[M][M],f[1<<M],ans;
bool v[N];
queue<int>q;
void bfs(int s){    //BFS 计算出所有标记点对之间的距离 
    for(int i=0;i<=n;i++) d[i]=1e18,v[i]=0;
    d[s]=0,v[s]=1,q.push(s);
    while(q.size()){
        int x=q.front(),y;q.pop();
        for(int i=1;i<=m;i++){
            if((y=x+l[i])<=n&&!v[y]) d[y]=d[x]+1,v[y]=1,q.push(y);
            if((y=x-l[i])>=0&&!v[y]) d[y]=d[x]+1,v[y]=1,q.push(y);
        } 
    }
    for(int i=0;i<=n;i++)
        if(b[i]) dis[c[s]][c[i]]=d[i];
}
signed main(){
    scanf("%lld%lld%lld",&n,&k,&m);
    for(int i=1;i<=k;i++)
        scanf("%lld",&x),a[x]=1;
    for(int i=1;i<=m;i++)
        scanf("%lld",&l[i]);
    for(int i=0;i<=n;i++)
        b[i]=a[i]^a[i+1],c[i]=(b[i]?g++:0);    //b 为差分序列。若节点 i 是标号点,也就是 b[i]=1,则节点 i 对应的标记点编号为 c[i](编号从 0 开始) 
    for(int i=0;i<=n;i++)
        if(b[i]) bfs(i);    //注意这里是 if(b[i]) 而不是 if(c[i]),因为标记点的编号是从 0 开始的 
    for(int s=1;s<(1<//状压 DP 
        f[s]=1e18;
        for(int i=0;i){
            if(!((s>>i)&1)) continue;
            for(int j=i+1;j//枚举 S 二进制下为 1 的两个位为 i,j 
                if((s>>j)&1) f[s]=min(f[s],f[s-(1<1<dis[i][j]);
        } 
    } 
    ans=f[(1<1],printf("%lld\n",ans==1e18?-1:ans);
    return 0;
}