#状态压缩入门#
这是我在HLUOJ上的第一篇blog,望多多指教0.0
我最近在学状态压缩,底下是一道入门题的题解,而具体的状态压缩经典题的题解详见杨神的blog。
友情链接:http://10.10.21.57/blogof/yangchongye-2020/blog/115
有时候我们没有办法将已经做过的决策序列s进行简化(比如s中的每一个决策都对后面的决策有影响),以至于我们需要在深搜的时候记录整个s,有时这个s可能是一个集合,这个时候就需要用到状态压缩。
状态压缩是指将一个集合用二进制数表示,二进制的第i位代表集合中有没有i这个数,例如集合{0,2,4}可以用 ( 10101 ) 2 (10101)_2 (10101)2这个数表示,这个数的十进制形式为21。这样我们就继续用数字作为下标表示状态了。
##例题(#486 种花小游戏)##
####题目描述####
植物大战僵尸这款游戏中,还有个特别有意思的赚钱方式——种花(能长金币的花)。种出来的金币需要玩家点击才能得到,或者,玩家可以购买一只蜗牛来帮助捡金币。然而,蜗牛爬得慢是众所周知的。所以,场上有若干金币时,蜗牛总是喜欢以最少的行程来捡走所有的金币。现在告诉你场上n个金币所在位置的坐标,以及蜗牛所在位置,让你求出蜗牛捡走所有金币的最小行程。
####输入格式####
第一行一个正整数n,表示金币数量。之后n行,每行两个非负整数x、y,分别表示金币所在位置坐标。最后一行两个正整数x、y表示蜗牛起始位置。
####输出格式####
一个实数(保留2位小数),表示最短行程。
####样例数据####
input
4
0 1
1 1
1 0
2 2
0 0
output
4.83
####说明####
(0,0)–>(1,0)–>(0,1)–>(1,1)–>(2,2)
1 + 1.414 + 1 + 1.414 = 4.83
####数据规模与约定####
对于20%的数据,n=3
对于70%的数据,n<=8
对于100%的数据,n<=16,x、y<=10000
时间限制:1s
空间限制:256MB
##题解##
这道题关键是对f二维数组,dp函数,状态转移方程的理解,再加上对一些玄学位运算的理解。
底下是关键中的关键:
for (int i=1;i<=n;++i)
if ((i!=start)&&((1<<(i-1))&now))
f[now][start]=min(f[now][start],s[i][start]+dp(now-(1<<(start-1)),i));
(1<<(i-1))&now)用来判断now这种状态二进制下第i位是不是等于1
如果是1,now-(1<<(start-1))表示将now这种状态二进制下第start位更改成0,有递归的思想。
也就是一步一步往前推,它要求以start为起点,达到now这种状态的最短行程,既然i!=start,那就往前推一步,先求从i出发到start。
底下是代码0.0
#include<bits/stdc++.h>
using namespace std;
const double maxn=2000000000.0;
int n,x[20],y[20],sum;
double s[20][20],f[66010][20],ans=maxn;//初始化ans
//f数组的第一维表示状态,因为是二进制下的(这道题应开到比2^16大)
//第二维表示以此为起点,达到第一维这种状态的最短行程
inline int read()//快读
{
int num=0,flag=1;
char c=getchar();
for (;c<'0'||c>'9';c=getchar())
if (c=='-') flag=-1;
for (;c>='0'&&c<='9';c=getchar())
num=(num<<3)+(num<<1)+c-48;
return num*flag;
}
double dp(int now,int start)//now表示当前状态,start表示当前点
{
if (f[now][start]<maxn) return f[now][start];//相当于搜索中的记忆化
for (int i=1;i<=n;++i)
if ((i!=start)&&((1<<(i-1))&now))
f[now][start]=min(f[now][start],s[i][start]+dp(now-(1<<(start-1)),i));
return f[now][start];//返回的值是以start为起点,达到now这种状态的最短行程,有递归的思想
}
void init()//输入
{
n=read();
for (int i=1;i<=n;++i)
{
x[i]=read();
y[i]=read();
}
x[0]=read();
y[0]=read();
}
void work()//主体程序
{
for (int i=0;i<n;++i)
sum+=(1<<i);
//(1<<i)表示将二进制的右数第(i+1)位更改成1
//sum表示将n位二进制的每一位更改成1
for (int i=1;i<=sum;++i)
for (int j=1;j<=n;++j)
f[i][j]=maxn;//初始化f数组
for (int i=1;i<=n;++i)
f[(1<<(i-1))][i]=0;
//(1<<(i-1))表示将二进制的右数第i位更改成1
//这里表示以i为起点,只到达i这个点这种状态的最短行程为0
for (int i=0;i<=n;++i)
for (int j=i+1;j<=n;++j)
{
s[i][j]=sqrt((x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j]));
s[j][i]=s[i][j];
}//计算包括蜗牛在内的每两个点之间的距离
for (int i=1;i<=n;++i)
ans=min(ans,s[0][i]+dp(sum,i));//计算蜗牛到i这个点+以i为起点经过所有点的最短行程
printf("%.2lf\n",ans);//输出答案
}
int main()
{
init();
work();
return 0;
}
T1 排列DP
让你去最优化一个排列使得相邻两个数的计算出来的某个东西的总和最大或者最小
Tip
这种题的套路就是我们设f[S][i]表示我们已经填了前面的若干个数,然后这若干个数它们代表的集合是S(不关心集合里面的相对顺序)并且最后一个数是i的最优的值,然后转移就是枚举下一个点拼上去
小trick
S二进制下1的个数?
__builtin_popcount(S)
T2 子集DP
我们需要寻找一个最佳的子集划分方式
Tip
我们的状态就是集合,然后转移方程就是枚举它的一个子集把它给划分出去,然后再接着递归
小trick
如何枚举S的子集T?
for (int T=S;T;T=(T-1)&S)
T3 轮廓线DP
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int T,n,m,cur;
long long f[2][200010];
inline int read()
{
int num=0,flag=1;
char c=getchar();
for (;c<'0'||c>'9';c=getchar())
if (c=='-') flag=-1;
for (;c>='0'&&c<='9';c=getchar())
num=(num<<3)+(num<<1)+c-48;
return num*flag;
}
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
T=read();
while (T--)
{
n=read();
m=read();
if (n<m) swap(n,m);
memset(f,0,sizeof(f));
f[0][(1<<m)-1]=1;
cur=0;
for (int i=0;i<n;++i)
for (int j=0;j<m;++j)
{
cur^=1;
memset(f[cur],0,sizeof(f[cur]));
for (int k=0;k<(1<<m);++k)
{
if (((k>>j)&1)==0)
{
(f[cur][k|(1<<j)]+=f[cur^1][k])%=mod;
continue;
}
if (j&&((k>>(j-1))&1)==0) (f[cur][k|(1<<(j-1))]+=f[cur^1][k])%=mod;
(f[cur][k&(~(1<<j))]+=f[cur^1][k])%=mod;
}
}
printf("%lld\n",f[cur][(1<<m)-1]);
}
fclose(stdin);
fclose(stdout);
return 0;
}