P3943 星空(狀壓DP+bfs+異或差分)
命運偷走如果只留下結果, 時間偷走初衷只留下了苦衷。
你來過,然後你走後,只留下星空。
題目描述
逃不掉的那一天還是來了,小 F 看著夜空發呆。
天上空蕩蕩的,沒有一顆星星——大概是因為天上吹不散的烏雲吧。
心裡吹不散的烏雲,就讓它在那裡吧,反正也沒有機會去改變什麼了。
小 C 拿來了一長串星型小燈泡,假裝是星星,遞給小 F,想讓小 F 開心一點。不過,有著強迫症的小 F 發現,這串一共 n 個燈泡的燈泡串上有 k 個燈泡沒有被點亮。小 F 決定和小 C 一起把這個燈泡串全部點亮。
不過,也許是因為過於笨拙,小 F 只能將其中連續一段的燈泡狀態給翻轉——點亮暗燈泡,熄滅亮燈泡。經過摸索,小 F 發現他一共能夠翻轉 m 種長度的燈泡段中燈泡的狀態。
小 C 和小 F 最終花了很長很長很長很長很長很長的時間把所有燈泡給全部點亮了。他們想知道他們是不是蠢了,因此他們找到了你,讓你幫忙算算:在最優的情況下,至少需要幾次操作才能把整個燈泡串給點亮?
解題思路
把未點亮的位置用1表示,點亮的位置用0表示
Step 1
首先有一些區間翻轉操作,考慮怎麼快速維護
有一種神奇的東東叫做異或差分,顧名思義就是每個位置和上個位置的異或值
例如原數列為
10010110
那麼差分數列為
110111010
注意差分數列要到第n+1位
那麼有了這個數列,在區間反轉時只需要轉換為對後面的影響
也就是在差分陣列第l位異或一,在第r+1位異或一
最終把差分陣列字首異或出來就可以得到原陣列
例如區間反轉2~5,
則差分陣列為100110010
得到的原陣列為11101110
可以發現正好反轉了
所以我們把題目給的原狀態的異或差分陣列求出,當差分陣列全部為0時,可知此時原陣列也全為0,也就是目標狀態
Step 2
那麼區間反轉操作可以看成將序列中兩個位置去翻,很容易得知如果這兩個位置都是0,那麼無意義
分兩種討論
1:一個為0,一個為1
那麼取反之後還是一個為0,一個為1,但是交換了位置,也可以看成1移動了位置
2:都為1
直接消掉就可以了
於是我們可以預處理出消除某兩個1的最少花費,也就是對於每種操作,在每個位置連一條邊權為1的邊,那麼兩點間的最短路就是最少花費,但是跑最短路有個log,較慢
我們知道邊權為1的最短路和bfs是等價的,且bfs是沒有log的,所以bfs就可以了
Step 3
迴歸正題,這個題是個狀壓DP
設F[S]表示要求點亮的位置的狀態,由於是在異或差分陣列中,所以要最多有$2\times k$個1
則狀態轉移就很簡單了,就是選取兩個沒被點亮的把它點亮,取最小花費就可以啦
#include<bits/stdc++.h>
using namespace std;
const int N = 4e4+7;
int n,k,m;
int a[N],pos[700],opt[700],d[N];
int dis[N],len[200][200];
int f[1<<(18)],tot=0;
queue<int> q;
bool vis[N];
void bfs(int st)
{
for(int i=1;i<=n+1;i++)
{
dis[i]=99999999;
vis[i]=0;
}
dis[st]=0;
vis[st]=1;
q.push(st);
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=1;i<=m;i++)
{
int l=x-opt[i];
int r=x+opt[i];
if(l>=1&&!vis[l])
{
dis[l]=dis[x]+1;
vis[l]=1;
q.push(l);
}
if(r<=n+1&&!vis[r])
{
dis[r]=dis[x]+1;
vis[r]=1;
q.push(r);
}
}
}
}
int main()
{
scanf("%d%d%d",&n,&k,&m);
for(int i=1;i<=k;i++)
{
int x;
scanf("%d",&x);
a[x]=1;
}
for(int i=1;i<=n+1;i++)
{
d[i]=a[i-1]^a[i];
if(d[i]) pos[++tot]=i;
}
for(int i=1;i<=m;i++)
scanf("%d",&opt[i]);
for(int i=1;i<=tot;i++)
{
bfs(pos[i]);
for(int j=1;j<=tot;j++)
{
len[i][j]=dis[pos[j]];
}
}
memset(f,12,sizeof(f));
f[(1<<tot)-1]=0;
for(int i=(1<<tot)-2;i>=0;i--)
{
for(int x=1;x<=tot;x++)
{
if(!((i>>(x-1))&1))
{
for(int y=x+1;y<=tot;y++)
{
if(!((i>>(y-1))&1))
{
f[i]=min(f[i],f[i+(1<<(x-1))+(1<<(y-1))]+len[x][y]);
}
}
}
}
}
cout<<f[0];
return 0;
}