一、前言
在做动规的时候有时会遇到一些有复杂状态的题目,这些状态如果不压缩,而只用数组的每一维来存储每一个状态,少则三至四维,多的话,甚至能达到九维或十维,这样不仅会耗费大量空间,且时间上也很难过。(本人的惨痛经历) 所以,我们不能傻乎乎地用这种笨方法,那我们应该怎么办呢?
话不多说,正文来了
二、正文
1、总述
一些题每一个点的状态只有两种,例如,一个地点是否走过,可以用“0”来表示未经过,用“1”来表示已经过,那么假设有5个“点”,第一和第三个“点”已经过,那我们可以用“10100”来表示,这个二进制数转化为十进制数后,就是“20”,那我们就能用“20”来表示这一状态。
我们其实可以用二进制数的每一位来代表每个“点”的状态,那就不需要用过多的数组下标来记录状态,这样的话,本来要用九维或十维的数组就能压缩到二维,甚至一维。(tips:题目n的数据大的话最好不要用这种方法)
说到二进制,就先来复习一下有哪些位运算。
2、位运算
当我们需要对状态进行操作或访问时,必须要对它的二进制位进行操作,这离不开位运算。
首先,我来介绍一下状压dp中常用的运算:
1、‘&’:含义:按位与 ;效果:相同位的两个数字都为1,则为1,若有一个数字不为1,则为0;例子:14(1110)&10(1010)=10(1010);
2、‘|’:含义:按位或;效果:相同位的两个数字只要其中一个为1,则为1;例子:11(1011)|10(1010)=11(1011);
3、‘^’:含义:按位异或;效果:相同位的两个数字不同时为1,否则为0;例子:13(1101) ^ 11(1011)=6(110);
4、‘<<’:含义:左移;效果:a<<2 相当于将a在二进制下的每一位向左移动两位,也就是在a的二进制数末尾加两个0(a<<n=a*(2的n次方));例子:5(101)<<2=20(10100);
5、‘>>’:含义:右移;效果:a>>2相当于将a在二进制下的每一位向右移动两位,也就是去掉a的二进制数末尾的2位(a>>n=a/(2的n次方));例子:10(1010)>>2=2(10);
接下来,就要讲一下它们在状压dp中常见的应用:
-
判断一个数字x第i位是否为1
方法:if(((1<<(i-1))&x)>0)
将1左移i-1位,相当于制造了一个第i位为1,其它位为0的数,再与x做与运算,如果结果>0,则说明x的第i位上是1,反之为0; -
将一个数字x二进制下第i位更改为1
方法:x=x|(1<<(i-1));
将1左移i-1位,相当于制造了一个第i位为1,其它位为0的数,再与x做或运算,则无论x的第i位原来为0或1,都会修改为1,其它位不变; -
把一个数x二进制下的最靠右的第一个1去掉
方法:x=x&(x-1);
数字x减去1后,假设x原长为k,则可分为以下3种情况:- x的所有二进制位上都为1,但长度变为k-1。
- x的最右边的一位由1变为0,但长度不变。
- x的第i位(1<i<k,如果有多个,则取最小的值)由0变为1,但长度不变。
那我们根据不同的情况来分析:
首先,第一种情况下,可分析得出:原来的x的二进制数只有最左侧的一位为1,进行与运算后,x变为0,例子:8(1000)&7(111)=0;第二种情况的话,不用说大家都懂,那就先不讲我才不会告诉你我懒得讲呢;第三种情况的话,首先我们来证明一下原x的第i位为最靠右的1:如果一个数y的第j位为0,减去1后它会向前1位“借”一个2,抵消掉减去的1后,这一位就变为了1,前一位就减了1(欠债不还的第j位数是屑)经过了多次变化后第i位后的数都变为1,只有它变为0,最后在做与运算,这样的话,后面的内容不用我说都行了。
好像有点离题,不管了,接下来我们来做做题练习一下
三、练习
原题:洛谷P1433
【题目描述】
房间里放着n块奶酪。一只小老鼠要把它们都吃掉,问至少要跑多少距离?老鼠一开始在(0,0)处。
【输入格式】
第一行有一个整数,表示奶酪的数量n。
第二到第(n+1)行,每行两个实数,第(i+1)行的实数分别表示第i块奶酪的横纵坐标xi,yi。
【输出格式】
输出一行一个实数,表示要跑的最少距离,保留2位小数。
【输入输出样例】
输入#1
4
1 1
1 -1
-1 1
-1 -1
输出#1
7.41
【说明/提示】
数据规模与约定
对于全部的测试点保证1≤n≤15,|xi|,|yi|≤200,小数点后最多有三位数字。
提示
对于两个点(x1,y1),(x2,y2),两点之间的距离公式为sqrt((x1-x2)2+(y1-y2)2)。
这是一道比较基础的状压DP题目。
这道题的数据不大,可以用状压。首先,我们先求出各个点之间的直线距离(tips:不需要求最短路径,反正两点之间线段最短)。接着,根据分析,我们发现,我们不仅要记录点的状态(有没有走过),还要记录当前所处的点(不记录的话,可能会导致小鼠现在在点n,然后它突然用从点m走到点x的路径到达了点x的情况发生。小鼠:我会飞雷神,你有意见? )
那我们应该怎么办呢,其实很简单,我们只需要用一个二维数组f[当前所处的点] [所有点的状态]来表示当前状态就行了。这样的话,状态转移方程也不难写。上代码:
#include<bits/stdc++.h>
using namespace std;
int n,wz[20];
double x[20],y[20],f[20][100005],jl[20][20],ans;
double js(double x1,double y1,double x2,double y2)
{
return sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
}
int main()
{
memset(f,127,sizeof(f)); //赋初值
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%lf%lf",&x[i],&y[i]);
for(int i=1;i<=n;i++)
f[i][1<<(i-1)]=js(0,0,x[i],y[i]); //边界条件
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++)
jl[i][j]=jl[j][i]=js(x[i],y[i],x[j],y[j]);
for(int i=1;i<(1<<n);i++)
{
int c=0;
for(int j=0;j<n;j++)
if(((1<<j)&i)==0)
wz[++c]=j;
for(int j=1;j<=n;j++)
{
if(f[j][i]==f[0][0]) continue;
for(int k=1;k<=c;k++)
f[wz[k]+1][i|1<<(wz[k])]=min(f[wz[k]+1][i|1<<(wz[k])],f[j][i]+jl[wz[k]+1][j]);
}
}
ans=f[0][0];
for(int i=1;i<=n;i++)
ans=min(ans,f[i][(1<<n)-1]);
printf("%.2lf",ans);
return 0;
}
四、总结
总而言之,大家只要找到可压缩的状态,就只用像做普通的动规题一样去做就行了,再见。