浅谈状态压缩

一、前言

在做动规的时候有时会遇到一些有复杂状态的题目,这些状态如果不压缩,而只用数组的每一维来存储每一个状态,少则三至四维,多的话,甚至能达到九维或十维,这样不仅会耗费大量空间,且时间上也很难过。(本人的惨痛经历) 所以,我们不能傻乎乎地用这种笨方法,那我们应该怎么办呢?
话不多说,正文来了

二、正文

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中常见的应用:

  1. 判断一个数字x第i位是否为1
    方法:if(((1<<(i-1))&x)>0)
    将1左移i-1位,相当于制造了一个第i位为1,其它位为0的数,再与x做与运算,如果结果>0,则说明x的第i位上是1,反之为0;

  2. 将一个数字x二进制下第i位更改为1
    方法:x=x|(1<<(i-1));
    将1左移i-1位,相当于制造了一个第i位为1,其它位为0的数,再与x做或运算,则无论x的第i位原来为0或1,都会修改为1,其它位不变;

  3. 把一个数x二进制下的最靠右的第一个1去掉
    方法:x=x&(x-1);
    数字x减去1后,假设x原长为k,则可分为以下3种情况:

    1. x的所有二进制位上都为1,但长度变为k-1。
    2. x的最右边的一位由1变为0,但长度不变。
    3. 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;
}

四、总结

总而言之,大家只要找到可压缩的状态,就只用像做普通的动规题一样去做就行了,再见。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值