zoukankan      html  css  js  c++  java
  • [笔记] [题解] 状压$DP$&洛谷P1433

    [笔记] [题解] 状压(DP)&洛谷P1433

    状压(DP)

    状压(dp)是动态规划的一种,通过将状态压缩为整数来达到优化转移的目的 -- (OI wiki)

    ​ 状态压缩的思想是用二进制来表示状态.

    ​ 状压(dp)的时间复杂度是(O(n^22^n))的,通常只能用于(n leq 21)的数据范围

    题目思路

    我们定义数组(dp[i][j])表示的是老鼠走到第(i)个奶酪,且之前走过的状态为(j)时所用的最短的距离.举个栗子:(dp[9][166])代表的就是老鼠走到了第(9)块奶酪,之前已经走过第(2,3,6,8)块奶酪,因为(166)用二进制表示就是(10100110),值得注意的是,这里的第(i)状态是从二进制低位到高位来算的.

    对于(dp)数组的初始化:首先先赋值为最大值,可以使用memset(dp,127,sizeof(dp)),接着如果只经过第(i)块奶酪,那么对应的(dp)数组的值就是原点到这个奶酪的坐标的直线距离.用程序表示就是dp[i][1 << (i - 1)] = dis[0][i].为什么是左移(i-1)位呢?我们可以手推一下,假设现在我们只到了第(1)块奶酪,那么对应的状态(二进制)因该是(0001),转换为十进制就是(1),(1=2^0),所以我们其实是左移(0)位,如果到了第(2)块奶酪,则二进制下的表示是(00010),对应的十进制就是(2),(2 = 2^1)所以实际上左移了(1)位,这也就是左移(i-1)位的原因.

    接下来就是转移方程了,(dp[i][k] = min(dp[i][k],dp[j][k-2^{i-1}] + dis[i][j])),(dp[j][k-2^{i-1}])表示的是现在老鼠在(j)点,并且没有走过(i)点的最短距离,(dis[i][j])就是(i,j)之间的距离,为什么可以表示没有走过(i)的状态呢?因为(dp[i][k])要求的就是走过了(i)的最佳答案,所以(k)的二进制表示中在(i)那一位是(1),而(dp[j][k-2^{i-1}])中,(k)减去了(2^{i-1})所以刚好把(k)二进制表示中(i)那一位的(1)减成了(0),至于为什么是(2^{i-1})前文有解释.

    关于统计答案,就是找出(min(dp[i][2^n-1]))就可以了,因为已经明确了最终的状态是全部奶酪都要吃,所以对于每一个(i)只要保证描述状态的那一维也就是(2^n-1)也就保证了每一个奶酪都被吃了,所以每一个(i)都是成立的合法解,所以取最小值就行.再理解一下,为什么(2^n-1)就可以保证代表每一个奶酪都被取到了呢?根据上文的描述我们知道奶酪(i)被取了就意味着在(dp)数组第二维描述状态的数的二进制位上第(i)位是(1),那么其实我们只要在统计答案的时候保证描述状态的每一位都是(1)即可,那么我们先假设(n=3),那么对应的最终状态的二进制表示因该是(111),十进制下就是(7),而(7=8-1,8=2^3,8)的二进制表示是(1000),那么再在(8)的二进制位上(-1),就变成(111),也就是答案了.

    这就是状压(dp)的基本思路和解法,其实应用过的范围不是很广,因为可以支持的数据范围不大,但是这种用二进制来优化状态表示和转移的方法还是很有学习价值的.

    解题代码

    下面就是洛谷上例题的代码了:

    在实现的时候还是要注意精度,因为要不然的话太小的小数比如(0.001)就会被计算成(0),导致(WA),所以尽量所有含有实数运算的函数和参数都要定义成(double)类型

    #include <bits/stdc++.h>
    using namespace std;
    double dis[20][20];
    struct point{
    	double x,y;
    }pos[20];
    double dp[20][34000];
    int n;
    double pow(double x){
    	return x * x;
    }
    double calc_dis(int x,int y){
    	return sqrt(pow(pos[x].x - pos[y].x) + pow(pos[x].y - pos[y].y));
    }
    int main(){
    	double ans;
    	memset(dp,127,sizeof(dp));
    	ans = dp[0][0];
    	scanf("%d",&n);
    	for(int i = 1;i <= n;i++){
    		scanf("%lf%lf",&pos[i].x,&pos[i].y);
    	}
    	pos[0].x = pos[0].y = 0;
    	for(int i = 0;i <= n;i++){
    		for(int j = i + 1;j <= n;j++){
    			dis[i][j] = dis[j][i] = calc_dis(i,j);
    		}
    	}
    	for(int i = 1;i <= n;i++)
    		dp[i][1 << (i - 1)] = dis[0][i];
    	for(int k = 1;k < (1 << n);k++){
    		for(int i = 1;i <= n;i++){
    			if((k & (1 << (i - 1))) == 0)continue;
    			for(int j = 1;j <= n;j++){
    				if(i == j)continue;
    				if((k & (1 << (j - 1))) == 0)continue;
    				dp[i][k] = min(dp[i][k],dp[j][k - (1 << (i - 1))] + dis[i][j]);
    			}
    		}
    	}
    	for(int i = 1;i <= n;i++)ans = min(ans,dp[i][(1 << n) - 1]);
    	printf("%.2lf
    ",ans);
    	return 0;
    }
    
  • 相关阅读:
    C#简单操作XML文件的增、删、改、查
    一个感觉还算可以的验证码生成程序
    安装aclocal1报错问题
    php中soap 的使用实例无需手写WSDL文件,提供自动生成WSDL文件类
    ofstream和ifstream详细用法[转]
    [原]C++ Soap客户端实例
    PHP中文件读、写、删的操作
    C++ Boost Thread 编程指南
    (转)虚函数和纯虚函数区别
    strcpy和memcpy的区别
  • 原文地址:https://www.cnblogs.com/czy--blog/p/13963972.html
Copyright © 2011-2022 走看看