P3943 星空
哇真的是,这道题目太妙了。想到了要用状压,想到了 bfs 的前置,没有用差分连起来,差分太妙了。
P3943 星空
题目大意:
给你一个 \(01\) 串,有 \(m\) 种长度可以反转,问最少几次可以将序列变为全一。
思路:
-
区间修改 \(\xRightarrow{差分}\) 单点修改。
-
bfs求两点间最短路+状压存储状态。
-
利用状压求最终答案。
1. 区间修改 \(\xRightarrow{差分}\) 单点修改
首先,上来我们会发现,题目是要让我们进行区间操作,但是区间操作十分的麻烦,我们就可以想到差分。
但是普通的差分还是不好处理,因为一般的差分思路就是我们记原数组为 \(a\),差分数组为 \(b\),则对于某一次区间取反操作 \([l,r]\),我们就转化成 \(b_l\) ^ \(1\) 和 \(b_{r+1}\) ^ \(1\)。之后 \(b\) 数组前缀异或处理后,再和 \(a\) 按位异或就好了。但是我们不确定要处理的区间,所以难以求解。
既然如此,那我们可以换个思路,就是所谓的“正难则反”。
我们可以考虑,我们想要的 \(b\) 数组的最终状态是什么,答案显然:若第 \(i\) 个位置未点亮,则 \(b_i\) 和 \(b_{i+1}\) 需要 \(=1\)。
聪明的人可能会想到两个未亮的灯相邻的特殊情况,对于这种情况:
未点亮:2,3
对应 a 数组:0 1 1 0 0 ……
那么对应的 \(b\) 应该为:
b:0 1 1 0 ……
1 1
=> 0 1 0 1 0 ……
你可以在存需要为 \(1\) 的位置的时候特判掉,也可以不用,下面会说(在 2. 的最后)。
并且,由于区间取反 \([l,r]\) 在 \(b\) 数组上体现为 \(b_l\) ^ \(1\) 和 \(b_{r+1}\) ^ \(1\),所以有以下三种转化:
- \(b_l\) 和 \(b_{r+1}\) 同为 \(1\):相消。
- \(b_l\) 和 \(b_{r+1}\) 其中一个为 \(1\):将某一端 \(1\) 移动到另一端。
- \(b_l\) 和 \(b_{r+1}\) 同为 \(0\):无需操作(无意义)。
所以综上,题目就被转化为了:
有 \(2\times n\) 个位置为 \(1\),有 \(m\) 种将 \(1\) 移动的长度,\(1\) 和 \(1\) 相遇则相消,问最少几步可以全消除。
注意!!这里有个坑,就是由于转化为差分数组,会出现 \(i+1\),所以总长度 \(n\) 要 \(+1\)
之后就很套路了。
2. bfs 求两点间最短路+状压存储状态。
可以注意到 \(n\) 只有 \(8\),在转化为差分数组之后也只有 \(2\times n = 16\),显然是再提醒用状压来解决,那怎么用?
如果想用状压,就意味着只能存储是否消除,无法存储位置信息,那我们可以通过某种与处理的方式来实现初始化吗?
答案是可行的,因为如果最终某两个 \(1\) 相消了,我们先将这两个彻底消除之后再考虑剩下的一定没有影响。
这样,我们就可以预处理出任意两个 \(1\) 相互消除的代价。
并且,设 \(a\) 与 \(b\) 相消的话,只需要让某一个动就好了,因为 \(a\) 向右,\(b\) 向左,在中间相遇,和 \(a\) 一路向右没有区别。
所以就可以以每一个点作为起点,做一遍 bfs (由于每一步的代价都是 \(1\),所以直接 bfs 就行),求出两两之间的最短路,并通过状压记录。
这里就可以发现为什么说不用在存需要为 \(1\) 的位置的时候将重合的位置特判掉,答案就是重合的位置最短路等于 \(0\) 不影响最终答案。
inline void bfs(int st)
{
memset(dis,0x3f,sizeof(dis));
memset(ck,0,sizeof(ck));
dis[st]=0; ck[st]=1; q.push(st);
while(!q.empty())
{
now=q.front(); q.pop();
for(int i=1;i<=m;i++)
{
cl(now,now-len[i]);
cl(now,now+len[i]);
}
}
}
……
………………
…………
int main()
{
…………
……………………
……
memset(f,0x3f,sizeof(f));
for(int i=1,ls;i<=num;i++)
{
bfs(w[i]);
for(int j=1;j<=num;j++)
{
if(i==j) continue;
ls=(1<<(i-1))|(1<<j-1);
f[ls]=min(f[ls],dis[w[j]]);
}
}
…………
……
}
3. 利用状压求最终答案。
最简单,最套路的一步,枚举状态,枚举上一次相互消掉的是哪两个位置,转移。
同时,由于一定是两两相消,所以任何有意义的状态一定有偶数个 \(1\),所以可以剔除掉不合法的状态,算是个小优化,不去除也不影响答案和复杂度的正确性。
inline bool asknum(int A){bool rt=0;while(A)rt^=1,A-=lowbit(A);return rt;}
//求当前这种状态有几个1。
……………
up=(1<<num)-1; f[0]=0;
for(int i=3,ls1,ls2;i<=up;i++)
{
if(asknum(i)) continue; //小优化
for(int j=1;j<num;j++)
{
ls1=1<<(j-1);
if((i|ls1)!=i) continue;
for(int k=j+1;k<=num;k++)
{
ls2=1<<(k-1);
if((i|ls2)!=i) continue;
f[i]=min(f[i],f[(i^ls1)^ls2]+f[ls1|ls2]);
}
}
}
Code:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define lowbit(x) (x&(-x))
inline int read(){
int rt=0; char g=getchar();
while(g<'0'||g>'9') g=getchar();
while(g>='0'&&g<='9') rt=(rt<<3)+(rt<<1)+g-'0',g=getchar();
return rt;
}
int n,num,m,up;
int w[17],f[(1<<16)];
inline bool asknum(int A){bool rt=0;while(A)rt^=1,A-=lowbit(A);return rt;}
int len[70];
int dis[40004],now;
bool ck[40004];
queue<int>q;
inline void cl(int now,int to)
{
if(1<=to&&to<=n&&!ck[to])
dis[to]=dis[now]+1,ck[to]=1,q.push(to);
}
inline void bfs(int st)
{
memset(dis,0x3f,sizeof(dis));
memset(ck,0,sizeof(ck));
dis[st]=0; ck[st]=1; q.push(st);
while(!q.empty())
{
now=q.front(); q.pop();
for(int i=1;i<=m;i++)
{
cl(now,now-len[i]);
cl(now,now+len[i]);
}
}
}
int main()
{
n=read()+1; num=read()<<1; m=read();
for(int i=1;i<=num;i+=2) w[i]=read(),w[i+1]=w[i]+1;
for(int i=1;i<=m;i++) len[i]=read();
memset(f,0x3f,sizeof(f));
for(int i=1,ls;i<=num;i++)
{
bfs(w[i]);
for(int j=1;j<=num;j++)
{
if(i==j) continue;
ls=(1<<(i-1))|(1<<j-1);
f[ls]=min(f[ls],dis[w[j]]);
}
}
up=(1<<num)-1; f[0]=0;
for(int i=3,ls1,ls2;i<=up;i++)
{
if(asknum(i)) continue;
for(int j=1;j<num;j++)
{
ls1=1<<(j-1);
if((i|ls1)!=i) continue;
for(int k=j+1;k<=num;k++)
{
ls2=1<<(k-1);
if((i|ls2)!=i) continue;
f[i]=min(f[i],f[(i^ls1)^ls2]+f[ls1|ls2]);
}
}
}
printf("%d",f[up]);
return 0;
}