浅谈一类dp问题----状压dp
___new2zy___
2018-09-28 07:07:42
## 浅谈一类dp问题----状压dp
### ----谨以此篇来纪念我可怜的状压水准
### 撞鸭?状压!(不定期更新)
由于要NOIp复赛了。。。发现自己状压的还很菜
于是乎昨天找了几道典型的例题来刷一刷
发现状压这东西很好玩,涉及了很多优美~~(玄学+毒瘤)~~的位运算
结果就是看半天题之后有了思路结果不会实现QAQ。。。
毕竟。。。窝的位运算学的还是很差的
不多说了,直接写正文好了~
=================================================================
#### 令人憧憬~~(发指)~~的撞鸭(状压)
那么,什么是状压呢?
听起来高大上,其实没什么
下面讲一讲我自己的理解吧(应该有专业术语的,可是我懒,不想度娘)
**状压,即状态压缩的简称,在数据范围较小的情况下,将每个物品或者东西选与不选的状态“压”成一个整数的方法**
通常采用二进制状压法,(对于**二进制状压**)即对于一个我们“压”成的状态,这个整数在二进制下中的1表示某个物品已选,而0代表某个物品未选
这样我们就可以通过枚举这些“压”成的整数来达到枚举所有的物品选与不选的总情况,通常我们称这些情况叫做子集,对于这个状态整数,通常设为s
(对于二进制状压)通过二进制下的位运算来达到判断某个物品选与不选的情况,再通过这个状态来进行一些其他的扩展,能简化我们对于问题的求解方式
而**状压dp正是用到了这一点,通过一个状态来表示整体情况,对于这个情况进行一些最优化操作,最终达到求得全局最优解的目的**
然而还有其他的状压方法,例如**三进制状压**法等等,本人在此就不再赘述
~~(其实是太菜不会讲,应该把机房某dalao拉过来讲一下的)~~
然而对于状压dp,本质上是与dp无异的
说白了,**状压dp,没有改变dp本质的优化方法,阶段还是要照分,状态还是老样子,决策依旧要做,转移方程还是得列,最优还是最优,无后性还是无后性**,所以它还是比较好理解的。
其实我觉得,状压dp可以理解为最暴力的dp,因为它需要遍历每个状态,所以在大多数题目中,会出现2^n的状态,所以最明显的标志就是数据不能太大(大约是$n<=16$)
遍历所有状态的正确姿势就是用二进制的位运算来遍历
对于一个二进制$01$串,$1$表示使用,$0$表示未使用,我们可以把所有的状态投射到很多二进制的数上(类似于hash的思想)
这大概就是状压dp的精髓了吧!
状压dp需要用到一些~~(很多)~~位运算的知识,下面给出一张图说明
~~显然是网上找的QAQ~~
![](https://i.loli.net/2018/09/28/5bad66dde29f2.png)
是不是感觉有上面的话有点熟悉?
~~(我是不会告诉你这是我从 [这里](https://www.luogu.org/blog/new2zy/solution-p3959) 搬过来的233)~~
那么下面给出一些昨天做过的状压例题结合说明一下
=================================================================
### ex1:luogu p1896 互不侵犯
题目传送门:
https://www.luogu.org/problemnew/show/P1896
很久以前就听说这题是个状压$dp$,但是总也不敢写$QWQ$
~~(貌似这已经是个状压$dp$的入门题了233)~~
咳咳。。。还是说正经的
题目很明确,一个国王只能攻击它相邻的$8$个格子($8$联通)
那么如果再次简化,可以发现其实是$6$联通
即:只要保证每一个国王的左上、上、右上、左、右都没有其他的国王就行了
那么再简化一步说明,每一行的状态只与上一行的状态有关
显然这符合$dp$的原则:**无后效性**
**设状态$f[i][j][k]$表示当前在第i行,第i行的状态为j,已经放置了k个国王时的方案数**
由于只与上一行有关,那么只需要某个放国王的位置满足6联通内无其他国王就行了
不妨设本行状态为$x$,上一行状态为$y$
当 $x$&$y$或$x$&$(y<<1)$或$x$&$(y>>1)$时显然不合法,舍去(存在上3联通国王)
再有,对于某一个状态$i$,显然当$i$&$(i<<1)$时不合法,舍去(存在相邻的国王)
那么这时对于这些合法的可能状态有:
$f[i][x][t+Count(x)]=∑f[i-1][y][t]$
其中$t$为上一行放的国王总数,$Count(i)$表示状态$i$在二进制下的$1$的个数
那么目标状态即为$∑f[n][x][K]$
还是挺好理解的对吧~
为了优化常数,我们可以先预处理出一些合法的状态,存在一个数组里
那么我们的dp状态就可以更改成$f[i][j][k]$表示对于第$i$行,当前状态**编号为**$j$时已经用了$k$个国王的总方案数
那么状态转移和上面一样啦
别看我写的这么顺畅,其实做的时候连位运算还不是太理解,到底在干什么。。。
还有就是注意开$longlong$就没什么了
下面放一下A掉的code好了
```cpp
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<queue>
#include<cstring>
#define re register
using namespace std;
typedef long long ll;
const int inf=1e9+7;
inline int read()
{
int p=0,f=1;re char c=getchar();
while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9'){p=p*10+c-'0';c=getchar();}
return f*p;}
const int maxn=11;
int n,K,lim,cnt,num[1<<maxn],can_use[1<<maxn];
ll f[maxn][1<<maxn][maxn*maxn],ans;
inline int Count(int s,int sum=0)
//计算状态s中有多少1(放了多少国王)
{
while(s)s&=(s-1),sum++;
return sum;
}
int main()
{
n=read(),K=read();
lim=(1<<n)-1;//满状态
for(re int i=0;i<=lim;i++)//预处理所有可行状态
if(!(i&(i<<1)))
can_use[++cnt]=i,num[cnt]=Count(i),f[1][cnt][num[cnt]]=1;
for(re int i=2;i<=n;i++)
for(re int j=1;j<=cnt;j++)
{
int x=can_use[j];
for(re int k=1;k<=cnt;k++)
{
int y=can_use[k];
if((x&y)||(x&(y<<1))||(x&(y>>1)))continue;
//两行之间状态不合法直接跳过
for(re int t=0;t<=K;t++)//累加方案
f[i][j][t+num[j]]+=f[i-1][k][t];
}
}
for(re int i=1;i<=cnt;i++)//将所有可行状态全部累加
ans+=f[n][i][K];
printf("%lld\n",ans);
return 0;
}
```
这题。。。可能很简单吧。。。~~(反正我写了1个多小时233)~~
貌似真的是一道很简单的状压dp了
===============================================================
### ex2:CF580D Kefa and Dishes
题目传送门:
https://www.luogu.org/problemnew/show/CF580D
感觉。。。这题是个很友好~~(毒瘤)~~的题了
翻译的人还好心的告诉我们要用状压。。。
这题在一般状压的基础上加了一点变化:状态之间有联系
题目中说:这$n$个菜有$k$个规则,如果kefa在吃完第$xi$个菜之后吃了第$yi$个菜(保证$xi$、$yi$不相等),那么**会额外获得**$ci$ $(0<=ci<=10^9)$的满意度
显然这是**与吃菜的顺序有关的**
但是再仔细考虑一下,会发现其实也是**没有后效性**的
因为**一道菜只与它前面的那道菜有关**
显然我们**可以枚举要吃的菜的前面那道菜**
不妨设状态$f[i][j]$表示当状态为$i$时,最后吃的一道菜为$j$时获得的最大满意度
如果设当前的状态为$x$,上一次的状态为$y$
显然对于一道我们要吃的菜$j$,在$x$中包含,在$y$中不能包含,而且必须对于某个菜$k$已经吃过
即:$i$&$(1<<j)$和$i$&$(1<<k)$时状态$i$才成立
同时当然也要保证$j!=k$
那么对于这些可行的状态有转移方程:
$f[i][j]=max(f[i$^$(1<<j)][k]+A[j]+w[k][j])$
显然当那些状态中存在$m$个$1$时(已经吃了$m$道菜)取$max$即为最后的答案
那么也就没什么了吧
下面放代码
```cpp
#include<iostream>
#include<cstdio>
#include<cmath>
#define re register
using namespace std;
typedef long long ll;
char IN[1<<17],*SS=IN,*TT=IN;
inline char gc(){return (SS==TT&&(TT=(SS=IN)+fread(IN,1,1<<17,stdin),SS==TT)?EOF:*SS++);}
inline int read()
{
int now=0,f=1;re char c=gc();
for(;!isdigit(c);c=='-'&&(f=-1),c=gc());
for(;isdigit(c);now=now*10+c-'0',c=gc());
return now*f;}
const int maxn=20;
int n,m,K,lim,A[maxn],mp[maxn][maxn];
ll f[1<<maxn][maxn],ans;
inline int Count(int s,int sum=0)
{
while(s)s&=(s-1),sum++;
return sum;
}
int main()
{
n=read(),m=read(),K=read();
for(re int i=0;i<n;i++)//初始状态:只吃一个
f[1<<i][i]=A[i]=read();
for(re int i=1;i<=K;i++)
{
int x=read()-1,y=read()-1,w=read();
mp[x][y]=w;
}
lim=(1<<n)-1;//满状态
for(re int i=0;i<=lim;i++)
{
int sum=Count(i);
for(re int j=0;j<n;j++)
{
if(!(i&(1<<j)))continue;//没吃过j不合法
for(re int k=0;k<n;k++)
{
if(j==k||(!(i&(1<<k))))continue;//相同或没吃过k不合法
f[i][j]=max(f[i][j],f[i^(1<<j)][k]+A[j]+mp[k][j]);
//dp转移方程
}
if(sum==m)ans=max(ans,f[i][j]);
//已经吃了m个菜就统计答案
}
}
printf("%lld\n",ans);
return 0;
}
```
这题还是很好理解的吧(至少比$ex1$好理解~)
===============================================================
### ex3:CF327E Axis Walking
题目传送门:
https://www.luogu.org/problemnew/show/CF327E
状压dp毒瘤题一道。。。
但是莫名感觉又占了一道黑题的便宜233~~(逃~~
这题很明了,一看就是状压dp
对于那些前缀和我们可以累加取得
不妨设$sum[i]$表示当状态为$i$时的前缀和
那么显然有$sum[i]=∑i$中为$1$的位置的$sum$
对于那些$sum[i]$为给定的数的状态显然要舍去
那么不妨再设$f[i]$为状态为$i$时的方案数
类似于$sum$的转移,显然有$f[i]=∑i$中为$1$的位置的$f$
发现这两者都需要访问那些状态$i$中的$1$的位置
那么对于这些状态$i$中那些为$1$的位置如何访问呢?
我们当然不能每次都让$i>>1$然后再判断这一位是否为$1$,因为这样太慢了
所以我们需要快速地访问那些$1$的位置
这时需要用到神奇~~(毒瘤)~~的位运算了
我们发现当$i$&~$lowbit(i)$时恰好能达到这一点
它**相当于把$i$最低位$1$以后的部分全部消去了**,相当于加速了我们的查找过程
于是乎,这题就与普通的状压dp无异了
放一下code吧
```cpp
#include<iostream>
#include<cstdio>
#define re register
using namespace std;
typedef long long ll;
const int inf=1e9+7;
char IN[1<<17],*SS=IN,*TT=IN;
inline char gc(){return (SS==TT&&(TT=(SS=IN)+fread(IN,1,1<<17,stdin),SS==TT)?EOF:*SS++);}
inline ll read()
{
ll now=0,f=1;re char c=gc();
for(;!isdigit(c);c=='-'&&(f=-1),c=gc());
for(;isdigit(c);now=now*10+c-'0',c=gc());
return 1ll*now*f;}
const int maxn=(1<<24)+3;
int n,k,lim,A[30],f[maxn];
ll no_permit[3],sum[maxn];
//f[i]表示状态为i时的方案数
//sum[i]表示状态为i时的前缀和
inline int lowbit(int x){return x&(-x);}
int main()
{
n=read();
for(re int i=0;i<n;i++)
sum[1<<i]=read();
k=read();
for(re int i=0;i<k;i++)
no_permit[i]=read();
f[0]=1,lim=(1<<n)-1;//满状态
for(re int i=1;i<=lim;i++)//枚举现在的状态
{
sum[i]=sum[i&~lowbit(i)]+sum[lowbit(i)];
if(sum[i]==no_permit[0]||sum[i]==no_permit[1])continue;
//不合法的状态排除
for(re int j=i;j;j-=lowbit(j))//枚举可能的上一次状态
f[i]=(f[i]+f[i&~lowbit(j)])%inf;
//快速累加所有可行的状态
//其中注意i&~lowbit(i)表示消去i二进制下lowbit后面的部分
//这样就可以直接快速访问那些为1的位置
}
printf("%d\n",f[lim]);
return 0;
}
```
可能。。。$lowbit$加速我现在也不太会用。。。
可以去看一看zsydalao写的题解 [链接](https://shehuizhuyihao.blog.luogu.org/solution-cf327e) 。。。可能我写的不是太好
大概是又水了一道黑题233
=================================================================
## 小结:
状压dp,一种神奇的、令人窒息的dp(大雾)
它可以让你当场秒懂,但实现起来懵逼且毫无头绪
尤其是令人作呕的位运算是它的一大杀手锏
但是我们相信,即使是再强的位运算也抵挡不住一颗勇往直前的心❤
或许当我真的成为一名有强大实力的OI选手,我可能会说:
“不就是状压dp么,看我秒切了它!”
可能这是后话了
stop!放一下鸡汤,还是说正经的。。。
虽说状压dp很难,也很不好想,但是NOIp近年确是有考到
比如
[宝藏](https://www.luogu.org/problemnew/show/P3959)
这题(还好我写过了233)
题目难度还是挺大的,也确实不是太好实现
可能想出了正解却不能很好地实现
况且状压不只应用在dp方面,其他的题目中也可能存在
比如一些题目,数据范围不小,根本不像是状压,但是却需要用到状压的思想来解决
这一点尤需注意
这篇状压的总结可能以后还会更新吧,但那些都是以后慢慢做的事情了
TSOI sjt 写于 2018年9月28日
别忘了给点个赞哟~QAQ
==============================================================
#### upd:2018年9月29日 上午
又找了一道 [马老师](https://www.luogu.org/space/show?uid=60124) 推荐的例题,早上切掉了233
~~(发现我现在写状压的题感觉很顺手)~~
### ex4:P3694 邦邦的大合唱站队
题目传送门:
https://www.luogu.org/problemnew/show/P3694
这题一看数据范围就知道是个状压
也没什么可说的,直接开讲好了
对于每个队伍,我们只关心它们最终有没有连在一起
不妨**设状态$f[i]$表示当所有的团队的连接状态为$i$时的最少出队人数**
显然,**目标状态**为$f[(1<<m)-1]$
那么不妨来思考一下如何进行$dp$转移
考虑到一个队伍$j$要连在一起,显然之前的状态是$i$中的第$j$位为$0$
即$i$^$(1<<j)$是i状态之前的状态,我们设为$k$好了
那么现在考虑一下,如何从状态$k$推到状态$i$呢
显然要把所有不是$j$队伍的人拿出队伍
那么拿出队伍的人数就是$num[j]-$属于$j$队伍在当前区间的人
对于这个当前区间内$j$队伍的人我们可以开一个二维数组$sum[x][i]$表示到位置$x$时队伍$i$的前缀和
那么**状态转移方程**为:$f[i]=$
$min(f[i$^$(1<<j)]+num[j]-(sum[now][j]-sum[now-num[j]][j]))$
那么。。。也就没什么了
注意数组下标就行了
```cpp
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#define re register
using namespace std;
typedef long long ll;
const int inf=1e9+7;
char IN[1<<17],*SS=IN,*TT=IN;
inline char gc(){return (SS==TT&&(TT=(SS=IN)+fread(IN,1,1<<17,stdin),SS==TT)?EOF:*SS++);}
inline int read()//fread一发入魂
{
int now=0,f=1;re char c=gc();
for(;!isdigit(c);c=='-'&&(f=-1),c=gc());
for(;isdigit(c);now=now*10+c-'0',c=gc());
return now*f;}
const int maxn=100003;
const int maxm=23;
int n,m,lim,num[maxm],sum[maxn][maxm];
int f[1<<maxm];
//f[i]表示当状态为i时(各队伍的排队情况)所需出队的最少人数
int main()
{
n=read(),m=read();
for(re int i=1;i<=n;i++)
{
int x=read()-1;
num[x]++,sum[i][x]++;
for(re int j=0;j<m;j++)//前缀和
sum[i][j]+=sum[i-1][j];
}
memset(f,0x3f,sizeof(f));
f[0]=0,lim=(1<<m)-1;//满状态
for(re int i=1;i<=lim;i++)//枚举状态
{
int now_len=0;
for(re int j=0;j<m;j++)//找到现在的队伍长度
if(i&(1<<j))now_len+=num[j];
for(re int j=0;j<m;j++)
if(i&(1<<j))
f[i]=min(f[i],f[i^(1<<j)]+num[j]-(sum[now_len][j]-sum[now_len-num[j]][j]));
//状态转移:显然是由某一个上次没排好的队伍转移过来,要去掉整体的减去该区间内原来有的人数
}
printf("%d\n",f[lim]);
return 0;
}
```
那么。。。貌似又水了一道状压dp。。。(大雾)
仍然留坑。。。下次做完状压dp还加到这里来
================================================================
#### upd:2018年9月29日 下午
开始疯狂做状压dp。。。又做了一道。。。
### ex5:P3475 [POI2008]POD-Subdivision of Kingdom
题目传送门:
https://www.luogu.org/problemnew/show/P3475
一看题面。。。感觉很清新
但是发现竟然。。。没有数据范围!
虽然说我是专门找的状压dp的标签。。。但是我也是很在意数据范围的
于是就~~厚颜无耻地~~看了一看题解。。。发现$n<=26$
“直接上状压!”
开始我开开心心的写了个正常的状压,暴力枚举每一个可能的情况
结果算了一下空间和时间。。。发现$T$了,而且是$T$到死。。
回过头来仔细思考,发现这题貌似不用枚举所有的状态
其实**只要枚举一半的状态**
因为题目中说要“分成两部分”
所以显然**一部分与另一部分的状态是相互对应的**
那么。。。就又想到一个神奇的思路
不妨枚举第一部分的状态为$s1$,第二部分的状态为$s2$
显然当$s1$中有$n/2$个$1$时代表整张图被分成两部分
那么我们只需要**找到一种情况使得在分成两部分的情况下相连的边数最少**就行了
不妨设初始时,$s1$中没有点,$s2$代表整个集合
那么每次我们可以枚举一个点,让它**在$s2$中删去,加入到$s1$中**
同时当前的**总边数更新**为$sum-$该点之前与$s1$的连边$+$该点之后与$s2$的连边
对于这个“连边数”,我们可以**预处理(总共一半的)每个状态内的$1$的个数**
那么$num[s>>13]+num[s-((s>>13)<<13)]$即为连边数
所以,大致思路就是状压$+dfs$
放一下代码好了
```cpp
#include<iostream>
#include<cstdio>
#define re register
using namespace std;
const int inf=1e9+7;
char IN[1<<17],*SS=IN,*TT=IN;
inline char gc(){return (SS==TT&&(TT=(SS=IN)+fread(IN,1,1<<17,stdin),SS==TT)?EOF:*SS++);}
inline int read()//fread一发入魂
{
int now=0,f=1;re char c=gc();
for(;!isdigit(c);c=='-'&&(f=-1),c=gc());
for(;isdigit(c);now=now*10+c-'0',c=gc());
return now*f;}
const int maxn=27;
int n,m,lim,p[maxn],num[(1<<(maxn>>1))+17],ans=inf,es,s1,s2;
inline int lowbit(int x)
{
return x&(-x);
}
inline void pre()//预处理一半的状态
{
lim=(1<<13)-1;
for(re int i=0;i<=lim;i++)
num[i]=num[i-lowbit(i)]+1;
}
inline int calc(int s)//计算集合中与另一个集合之间的连边数
{
return num[s>>maxn/2]+num[s-((s>>maxn/2)<<maxn/2)];
}
inline void dfs(int x,int k,int sum)
{
if(k==n/2)//已经分成了两部分:统计最优解
{
if(sum<ans)ans=sum,es=s1;
return ;
}
for(re int i=x+1;i<=n;i++)
{
int sum1=calc(p[i]&s1),sum2=calc(p[i]&s2);
//计算连边数
s1|=1<<(i-1);//从1集合加入i
s2^=1<<(i-1);//从2集合删去i
dfs(i,k+1,sum-sum1+sum2);
//更新状态继续搜索
s1^=1<<(i-1);//回溯
s2|=1<<(i-1);
}
}
int main()
{
n=read(),m=read();
pre();//预处理状态中的1的个数
for(re int i=1;i<=m;i++)
{
int x=read(),y=read();
p[x]|=1<<(y-1);
p[y]|=1<<(x-1);
}
s2=(1<<n)-1,dfs(0,0,0);
for(re int i=1;i<=n;i++)
if(1<<(i-1)&es)printf("%d ",i);
return 0;
}
```
其实这题还是挺好的
PS:~~不折半预处理或者数组开大了会T和M到死~~
=============================================================
#### upd:2018年9月29日 下午
### ex6:P3092 [USACO13NOV]没有找零No Change
题目传送门:
https://www.luogu.org/problemnew/show/P3092
~~(一道几乎是被我秒切的题)(逃~~
~~励志刷完所有的状压!~~(大雾)
可能我要做状压题做到死了233
不过真的很好想~~(感觉状压dp都是套路题)~~
这不,又~~(水)~~做完一道
这个题就是典型的状压$dp$了(一看题面就能看出来)
(貌似这是我15分钟写完的一道题了,不得不说真的好快。。。)
下面直接开讲(还有我的心路历程)
“一看K(硬币数)的范围这么小,肯定是个状压$dp$”
既然明确了是个状压$dp$,那么想一想如何设计状态和进行转移呢
题目中要求:**买完所有的物品之后剩下的最多钱数**
显然要设计一个状态
由于题目$K$很小$(K<=16)$,所以我们从$K$(硬币)下手
不妨**设状态$f[i]$表示使用硬币状态为$i$时能买到的最多商品数**
显然**目标状态为**$f[i]==n$时$min($总硬币价值$-i$中所用的硬币价值之和$)$
对于**一个硬币$j$,如果它在$i$状态中使用过**(对应位为$1$)
那么我们**可以由没用硬币$j$的状态转移到状态$i$**,即状态$(i$^$(1<<j))$
考虑到**花费硬币$j$后能买到的商品一定是所有商品中连续一段前缀**
其实**能买到的商品数就是这个前缀最后一个元素的下标**
那么到这里,$dp$的**转移方程**也就呼之欲出了:
$f[i]=max(f[i$^$(1<<j)]+$**在原来的基础上**使用硬币$j$最多能买到的商品数$)$
诶?这个“**在原来的基础上**”指的是什么呢?
其实**指的是在状态$i$^$(1<<j)$之前就已经买过的商品的总价值**
(已经买的商品显然就不用再买了啊QAQ)
而这些之前的总价值在数列中可以表示为连续的一段,即**前缀和**
那么上面其实就已经是一个可以实现的多项式做法了
但其实上式还可以优化
对于那些所有的满足条件的状态$i$^$(1<<j)$,都**存在着很多满足条件的原基础**,如果暴力的去扫描一个合理的最大位置,显然单次转移是$O(n)$的,那么总复杂度就是$O(2^K*n)$的,时间上不能接受
但因为我们要找“**最多能买到的商品数**”,显然是**取第一个小于等于"“原基础花费”+该硬币$j$"这个值的位置**
其实也就是前缀和数组中$sum[f[i$^$(1<<j)]]+coin[j]$的$upper$_$bound$
**为什么可以在前缀和数组中二分查找呢**?
因为每个商品最小代价为$1$,就保证了**前缀和数组单调上升**,所以**是可二分的**
那么对于所有这些可行的状态$i$^$(1<<j)$,我们就不必再一个一个枚举可行的最大位置,而是直接寻找上式的upper_bound就行了,单次转移的复杂度就降为了$O(logn)$,那么总复杂度为$O(2^k*logn)$
可能。。。到这里这题就讲完了吧。。。
这个优化还是挺重要的,没有它可能就会$T$到死
还有嘛。。。就是最开始我的二分初值打错了,没赋$0$,结果$RE$了一次
在函数内的变量也要注意初值,其实并不是$0$,小心哟!
下面放一下代码好了
```cpp
#include<iostream>
#include<cstdio>
#include<cmath>
#define re register
using namespace std;
typedef long long ll;
const ll inf=7e9+7;
char IN[1<<17],*SS=IN,*TT=IN;
inline char gc(){return (SS==TT&&(TT=(SS=IN)+fread(IN,1,1<<17,stdin),SS==TT)?EOF:*SS++);}
inline int read()//fread一发入魂
{
int now=0,f=1;re char c=gc();
for(;!isdigit(c);c=='-'&&(f=-1),c=gc());
for(;isdigit(c);now=now*10+c-'0',c=gc());
return now*f;}
const int maxK=18;
const int maxn=100003;
int K,n,tot,lim,A[maxK],sum[maxn],f[1<<maxK];
ll ans=inf;
inline int Binary_search(int x)//(upper_bound)二分查找第一个<x的数
{
int l=1,r=n,mid,p=0;//注意p的初值!不为0 #8会挂!
while(l<=r)
{
mid=(l+r)>>1;
if(sum[mid]<=x)l=mid+1,p=mid;
else r=mid-1;
}
return p;
}
int main()
{
K=read(),n=read();
for(re int i=0;i<K;i++)
{
A[i]=read();
tot+=A[i];
}
for(re int i=1;i<=n;i++)
sum[i]=sum[i-1]+read();
lim=(1<<K)-1;//满状态
for(re int i=0;i<=lim;i++)
{
for(re int j=0;j<K;j++)
if(i&(1<<j))//用过这个硬币
{
int pos=Binary_search(A[j]+sum[f[i^(1<<j)]]);
//在之前的状态中找到一个买东西数最多的状态更新
f[i]=max(f[i],pos);
}
if(f[i]==n)//当前状态恰好能买完所有东西
{
ll t=0;
for(re int j=0;j<K;j++)//累加所有用过的硬币
if(i&(1<<j))t+=A[j];
ans=min(ans,t);
}
}
if(ans>tot)printf("-1\n");//钱不够当然无解
else printf("%lld\n",tot-ans);//反之为最优解
return 0;
}
```
这题也不错,是一道状压$dp$练手好题
====================================================================
upd:2018年10月1日 上午
### ex7 P3451 [POI2007]ATR-Tourist Attractions
题目传送门:
https://www.luogu.org/problemnew/show/P3451
一道毒瘤卡空间题。。。调了一下午$+$一上午
本来还想早点写完之后看看别的东西来着~~(貌似这是状压dp的最后一题了)~~
不过不得不说题目还是很好的~~(虽然说我被卡了空间没过掉233)~~
来讲一下吧
看完这题我的心里是拒绝的。。。
题目很长,翻译不好,第一感觉题目很复杂
仔细读完题发现大概是有了思路
题目大意:
给你一张有$n$个点的无向图,起点为$1$,指定前$K$个点,要求按照必须给定的顺序经过前$K$个点(即对于一个点,必须先经过它之前的一些点再经过它),这些点可以重复经过,之后再到走$n$点,求整个过程的最短路
我们规定,$K<=20$且$n<=20000$
显然,这题和最短路有关,于是先从最短路下手
这些点是可以重复经过的,那么**对于一个指定的点,显然它之前的点怎么走都行**
这就需要求出两点之间的最短路了
可以用$SPFA$,也可以用堆优化的$diji$
那么又发现$K$很小,显然可以状压
不妨设状态$f[i][j]$表示经过指定的点状态为$i$,最后一个到达的点为$j$时的最短路
显然,这个状态没有后效性,可以继续递推出下一个状态
不妨枚举一个在$i$状态上下一个要去的点,设为$k$
显然$j!=k$(题目保证没有自环)
而且$k$必须满足所有它之前的点都被经过(已经属于状态集合中)才能进行转移
那么可以写出这样的转移方程:
$f[i|(1<<k-2)][k]=min(f[i][j]+dis[j][k])$
那么就可以转移了
目标状态即为$min(f[(1<<K)-1][i]+dis[i][n])$
(别忘了最后还要到$n$点)
特殊的,当$K=0$时(没有指定点)显然就是$dis[1][n]$直接输出即可
那么就可以做辣!
放一下代码好了(注意是$80$分,因为题目卡空间,过不掉)
```cpp
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<queue>
#include<cstring>
#define MP(x,y) make_pair(x,y)
using namespace std;
typedef long long ll;
const int inf=1e9+7;
inline int read()
{
int p=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9'){p=p*10+c-'0';c=getchar();}
return f*p;}
const int maxn=20001;
const int maxm=200001;
const int maxK=20;
struct Edge
{
int from,to,w;
}p[maxm<<1];
int n,m,K,Q,cnt,lim,ans=inf,head[maxn],E[maxK+1],vis[maxn];
int d[maxK+1][maxK+1],dis[maxn],f[(1<<maxK)+1][maxK];
//f[i][j]表示状态为i时最后到达的点为j时的最短路
inline void add_edge(int x,int y,int W)
{
cnt++;
p[cnt].from=head[x];
head[x]=cnt;
p[cnt].to=y;
p[cnt].w=W;
}
inline void SPFA(int s)//其实这是堆优化之后的diji= =
{
priority_queue<pair<int,int> > q;
memset(vis,0,sizeof(vis));
memset(dis,0x3f,sizeof(dis));
dis[s]=0,q.push(MP(0,s));
while(!q.empty())
{
int x=q.top().second;
q.pop();
if(vis[x])continue;
vis[x]=1;
for(int i=head[x];i;i=p[i].from)
{
int y=p[i].to,t1=dis[x]+p[i].w;
if(dis[y]>t1)dis[y]=t1,q.push(MP(-dis[y],y));
}
}
for(int i=1;i<=K+1;i++)
d[s][i]=dis[i];
d[s][0]=dis[n];
}
int main()
{
// printf("%.2lfMB\n",(double)sizeof(f)/(1<<20)+(double)sizeof(d)/(1<<20));
n=read(),m=read(),K=read();
for(int i=1;i<=m;i++)
{
int x=read(),y=read(),w=read();
add_edge(x,y,w);
add_edge(y,x,w);
}
if(!K){SPFA(1);printf("%d\n",d[1][0]);return 0;}
for(int i=1;i<=K+1;i++)//预处理最短路
SPFA(i);
Q=read();
while(Q--)//统计连边数
{
int x=read(),y=read();
E[y]|=1<<(x-2);
}
memset(f,0x3f,sizeof(f));
f[0][1]=0,lim=(1<<K)-1;//初始状态和满状态
for(int i=0;i<=lim;i++)//枚举状态
{
for(int j=1;j<=K+1;j++)//枚举上一次最后到达的城市
if(f[i][j]<1e9)
{
for(int k=2;k<=K+1;k++)//枚举这次要去的城市
{
if(j!=k&&(i&E[k])==E[k])//满足条件
{
int t=i|(1<<(k-2));//新状态
f[t][k]=min(f[t][k],f[i][j]+d[j][k]);//更新为更优解
}
}
}
}
for(int j=1;j<=K+1;j++)//最终状态统计答案
if(f[lim][j]<1e9)
ans=min(ans,f[lim][j]+d[j][0]);
printf("%d\n",ans);
return 0;
}
```
一道$zz$题卡了差不多一天,最后也没做完。。。
上面的代码已经卡了一页多了。。。并不能$AC$
~~就算我A掉了这题吧~~(雾)
==============================================================
## 总结与反思:
发现状压$dp$很~~(毒瘤)~~好玩,总计刷了$3$天的状压题
可能还是有些题目我没见过,但是我觉得已经见过足够多的状压套路了
做了这么多经典的状压题目,我感觉状压是一种思想,一种处理问题的方法,而不是简简单单的只局限在$dp$方面
感觉。。。最深的体会可能也就是这句话了(对于状压$dp$):
**“状压dp,没有改变dp本质的优化方法,阶段还是要照分,状态还是老样子,决策依旧要做,转移方程还是得列,最优还是最优,无后性还是无后性”**
毕竟**状压$dp$的本质还是$dp$,所以状压唯一的不同之处就在于使用位运算来简化条件和状态(而且数据范围很小)**
其实反过来想,所有的$dp$不都是这个样子么?
又想起之前听过的一段话:“**动态规划,本质就是一种简化问题的思路和方法,都是在原问题的基础上,忽略次要矛盾,关心主要矛盾,通过状态之间的转换和递推达到最终求解的目的**”
发现刷过一些状压$dp$的题后,对$dp$这种感觉和理解更加彻底了
大概所有的$dp$题都是这样的一个思想和套路了吧$QAQ$
况且,为了刷状压题,我发现我使用位运算的能力也提高了~~
(这也可能是我之前太菜了位运算用不好的原因)~~
一点不足就是,由于我做的题目都是状压$dp$,因此状压与其他算法的结合类的题目我见过和做过的和很少~~(甚至没有)~~
看来以后还是要多做一些状压题啊
$TSOI$ $sjt$ 写于$2018$年$10$月$1$日
~~(这可能是一个永远也填不完的坑了)~~