有关状压DP
【以下内容仅为本人在学习中的所感所想,本人水平有限目前尚处学习阶段,如有错误及不妥之处还请各位大佬指正,请谅解,谢谢!】
引言
动态规划虽然已经是对暴力算法的优化,但在某些比较特别的情况下,可以通过一些小技巧进一步对其优化,通产我们会在时间与空间中做权衡,在时间可以接受度范围内,适当的以时间为代价换取更小空间的占用;在不爆空间的情况下,适当的以空间换时间。在此,本人将以目前总结的经验详细介绍状态压缩与状压DP。
状态压缩
(一)状态
状态指某个事物表现出来的形态(百度百科)。联系前面的文章(有关动态规划 - PaperHammer - 博客园 (cnblogs.com)),我们在分析解决动态规划的问题时,其重点是在“分情况定变量”这一步,即有多少种选择,如何选择。不妨把这每一个选择视为一个事物,则它表现出来的形态就是所做出的选择,更进一步就是选择的结果。所以,动态规划中的状态实际上就是每一种情况。
(二)压缩
压缩是一种通过特定的算法来减小计算机文件大小的机制,其目的是减少所占空间的同时提高运算速度(百度百科)。压缩的本质是减小,但不可否认虽然减小了文件体积提高了传输速度,但会存在文件质量的下降。
(三)状态压缩
一般地,我们规定利用计算机的二进制性质来描述所做出的选择,即将所有情况统一分为:行或不行、放或不放等两种情况(将很多情况归于两类情况)。由此可以发现,对于状态压缩,我们是针对有多少种选择进行了压缩,即对空间进行优化,那么根据互斥性原则(自己编的名字),对空间上的优化势必会带来时间上的复杂。所以,状态压缩在时间上其实是一种很暴力的思想,它需要遍历每一种情况,每种情况有两种选择(0或1),最高会达到2n的时间复杂度,但针对一些题目,可以依靠某些限定条件,大幅降低时间复杂度,从而变相达到既优化了空间也优化了时间的目的。
【注:相关位运算内容在此不作说明】
举例
(一)吃奶酪
链接:P1433 吃奶酪 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题意:房间里放着n块奶酪。一只小老鼠要把它们都吃掉,问至少要跑多少距离?老鼠一开始在(0,0)点处。
分析:据题意可将其抽象为,从原点出发,返回走过所有点的最小距离。最值问题容易想到搜索。搜索时跳过重复路径,最后进行比较即可。
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5
6 namespace ConsoleApp1
7 {
8 internal class Program
9 {
10 static int n;
11 static double res = double.MaxValue;
12 static NodeToNode[][] ntn;
13 static int[] vis;
14 static void Main(string[] args)
15 {
16 Program p = new Program();
17 n = int.Parse(Console.ReadLine());
18 n += 1;
19 double[] x = new double[n], y = new double[n];
20 x[0] = 0;
21 y[0] = 0;
22 for (int i = 1; i < n; i++)
23 {
24 string[] inp = Console.ReadLine().Split(' ');
25 x[i] = double.Parse(inp[0]);
26 y[i] = double.Parse(inp[1]);
27 }//录入所有点,包括(0,0)
28 ntn = new NodeToNode[n][];
29 for (int i = 0; i < n; i++) ntn[i] = new NodeToNode[n];
30 for (int i = 0; i < n; i++)
31 {
32 for (int j = 0; j < n; j++)
33 {
34 ntn[i][j].dis = Dis(x[i], y[i], x[j], y[j]);//计算每两个点间的距离并编号
35 ntn[i][j].next = j;
36 }
37 ntn[i] = ntn[i].OrderBy(k => k.dis).ThenBy(k => k.next).ToArray();//一定要注意这个排序方式!!!!!
38 }
39
40 //for (int i = 0; i < n; i++)
41 // for (int j = 0; j < n; j++)
42 // Console.WriteLine(ntn[i][j].dis + " " + ntn[i][j].next);
43
44 vis = new int[n];
45 vis[0] = 1;
46 p.Dfs(0, 0.0, 1);
47 Console.WriteLine(res.ToString("0.00"));
48 //Console.ReadLine();
49 }
50 public void Dfs(int cur, double sum, int cnt)
51 {
52 if(cnt == n)
53 {
54 res = res <= sum ? res : sum;
55 return;
56 }
57 if (sum >= res) return;
58 for(int i = 0; i < n; i++)
59 {
60 if(vis[ntn[cur][i].next] == 0 && cur != ntn[cur][i].next)
61 {
62 vis[ntn[cur][i].next] = 1;
63 Dfs(ntn[cur][i].next, sum + ntn[cur][i].dis, cnt + 1);
64 vis[ntn[cur][i].next] = 0;
65 }
66 }
67 }
68 public struct NodeToNode
69 {
70 public double dis;
71 public int next;
72 }
73 private static double Dis(double x1, double y1, double x2, double y2)
74 {
75 return Math.Sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
76 }
77 }
78 }
这种思路本质上是对除原点以外的所有点,进行全排列,比较每一种排列结果,其时间复杂度为O(N*N!),(应该是),在比赛中一般仅能承受N<=10的数据范围。
再分析:因为在深度搜索中会出现大量重复访问的数据,所以优化搜索算法,最先想到的一般是记忆化处理,记住到达某一个点的最小距离。但这种方法不能保证最坏的情况,有一部分情况和之前无异。本题除了距离因素,就只剩奶酪因素,既然记住距离还不够,那就只能在“奶酪”上下手了。
对于每个奶酪,只有两种情况:吃过/没吃过;抽象出来即,对于每个点:走过/没走过。发现对于每个事物(奶酪/点),仅有两种状态(访问过/没访问过),满足基本状态压缩的前提,所以我们考虑使用二进制来表示每个状态。
a. 大化小:最终问题是让总距离最小,最终距离是基于每一次选择得来的,那么就把从一个点到一个新的点,这一选择视为一个子问题。并且每次选择总以最小最小距离为基础进行操作,每次选择处理方式相同。
b. 分情况定变量:对于每个点,只有两种情况(走或不走);我们需要知道当前这个点是否走过,所以要记录当前所在点;还需要知道已经走过了几个奶酪,所以要记录对点(奶酪)的访问情况,共两个变量。用f[i][j]表示老鼠当前走到第i个点(奶酪)处,且走过的点的二进制状态为j时,最短的距离。
如:可以使用二进制10100110来表示已经走过第2、3、6、8个奶酪(定义此处索引从1开始),此时j的值为166。需要注意的是,第i个状态是从低位向高位的第i位,即从低位向高位进行转移。
c. 推方程:如果要走这个点(保证该点可访问)那么f[i][j] = f[j][k - (1 << (i − 1)] + dis(i, j)其中f[i][j]表示以i为起点走成状态j的最小距离,dis表示两点间距离。
【解释 k - (1 << i − 1) 】
(1) 符号‘<<’:表示某十进制数对应的二进制数向右移,等价于对十进制数*2。
如:1 << 3表示将1对应的二进制编码向右移动4位 (1)10 = (0001)2 右移5位变为(1000)2 = (8)10 = 1 * 23
(2)式子:最终目标的状态 – 当前位置的状态 = 中间状态
如:(5)10 =(0101)2 = (0111)10 – (0010)2 = (7)10 - (2)10
上一个中间状态 = 目标状态 – 当前位置状态
【难点】为什么可以用 k & (1 << (i – 1)判断合法位置
我们设一个状态 k = 01101,表示第一、三、四列(从低位开始)中的某个点已经访问过;
由于我们是一行一行访问的,所以在k状态我们应该访问第三行的点了,那第三行我们应该访问哪个点,或者说状态k由哪些状态转移而来呢?
状态k(01101)由三种状态(必然是前两行的状态)来的:
前两行在三、四列已访问,第三行只好访问第一列;(01100)
前两行在一、四列已访问,第三行只好访问第三列;(01001)
前两行在一、三列已访问,第三行只好访问第四列;(00101)
无非就是这三种情况,现在我们来考虑怎么来表示状态s由这三种状态来的,k & (1 << (i - 1))就是用来实现这个功能的,即判断当前情况是否为其上一个情况转移而来。
for (int i = 1; i <= 4; i++) 01101 & (1<<0) = 01101 & 1 = 00001 01101 & (1<<1) = 01101 & 10 = 00000 01101 & (1<<2) = 01101 & 100 = 00100 01101 & (1<<3) = 01101 & 1000 = 01000 01101 & (1<<4) = 01101 & 10000 = 00000
由此得出,只有和k=01101有1重合结果才大于0,根据这个特性判断此列是否可以访问。
d. 定边界:本题在搜索过程中不存在索引非法而导致的无法访问点,但需要对初值进行设定,因为其默认值为0,我们需要存储最小距离,所以应对初值设定为一个较大的值。
【注:文末代码中已附有更加详细的注释】
(二)01背包
【注:题目解析请转至该文章有关动态规划 - PaperHammer - 博客园 (cnblogs.com)】
在该问题中,对于每种物品只有两种选择,放与不放,故也可以使用状态压缩进行优化。
如:有5件物品,
如果这5件物品都不放的话,那就是00000;
如果这5件物品都放的话,那就是11111;
观察可知,在上面的例子中00000 ~ 11111可以代表所有的情况,转化为十进制就是0到(1 << (5 – 1));
其中,所以f[10000]只能从f[00000] + W[1] 转移过来;f[11000]可以从f[01000] + W[1]或者f[10000] + W[2]转移过来,以此类推
总结
1. 注意位运算的运算等级顺序,括号很重要
2. 状压dp的特点一般是规模比较小,一般小于15,最多不超过20;而且一般只有两种决策
状态压缩比较难理解,本篇文章也花了快两周时间,虽然写成了笔记,但本人理解程度依旧不深,在此希望与各位大佬相互交流一起学习
【感谢您可以抽出时间阅读到这里;受限于水平,内容可能会有许多不妥之处,许多地方可能存在错误,还请各位大佬留言指正,请见谅,谢谢!】
#附文中所提到的第一题的代码 及 状压模板
(1)吃奶酪
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5
6 namespace ConsoleApp1
7 {
8 internal class Program
9 {
10 static int n;
11 static double res = double.MaxValue;
12 static double[][] f;
13 static double[] x, y;
14 static void Main(string[] arg)
15 {
16 n = int.Parse(Console.ReadLine());
17 x = new double[20];
18 y = new double[20];
19
20 for (int i = 1; i <= n; i++)
21 {
22 string[] inp = Console.ReadLine().Split(' ');
23 x[i] = double.Parse(inp[0]);
24 y[i] = double.Parse(inp[1]);
25 }
26
27 //初始化,因为之后的比较是取较小的一个,所以将所有值预设为最大
28 f = new double[20][];
29 for(int i = 1; i <= n; i++)
30 {
31 f[i] = new double[35000];
32 Array.Fill(f[i], double.MaxValue);
33 }
34
35 DP(f, n);
36 Console.WriteLine(res.ToString("0.00"));
37 //Console.ReadLine();
38 }
39 static public double DP(double[][] f, int n)
40 {
41 for (int k = 1; k <= (1 << n) - 1; k++)
42 for (int i = 1; i <= n; i++)
43 {
44 if ((k & (1 << (i - 1))) == 0) continue;//已经访问过
45 if (k == (1 << (i - 1)))
46 {
47 f[i][k] = 0;//标记为不可访问
48 continue;//当前位置与目标位置相同
49 }
50 for(int j = 1; j <= n; j++)
51 {
52 if ((k & (1 << (j - 1))) == 0 || i == j) continue;//如果不可访问 或 两个位置相同 则跳过
53 f[i][k] = Math.Min(f[i][k], f[j][k - (1 << (i - 1))] + Dis(i, j));
54 }
55 }
56 for (int i = 1; i <= n; i++)
57 {
58 double cur = f[i][(1 << n) - 1] + Dis(i, 0);
59 res = Math.Min(res, cur);
60 }
61
62 return res;
63 }
64 private static double Dis(int a,int b)
65 {
66 return Math.Sqrt((x[a] - x[b]) * (x[a] - x[b]) + (y[a] - y[b]) * (y[a] - y[b]));
67 }
68 }
69 }
(2)一般状压模板
1 int n;
2 int maxn = 1 << n;//总状态数。
3 //枚举已有的集合数。按照状态转移的顺序,一般从小编号到大编号。
4 for(int i = 1; i <= m; ++ i){
5 //枚举当前集合中的状态。
6 for(int j = 0; j < maxn; ++ j){
7 //判断当前集合是否处于合法状态,通常我们需用一个数组提前处理好。如g数组;
8 if(当前状态是否合格){
9 for(int k = 0; k < maxn; ++ k){
10 //枚举上一个集合的状态。
11 if(上一个集合的状态是否合格 + 上一个集合的状态和当前状态的集合是否产生了冲突){
12 列写状态转移方程。
13 }
14 }
15 }
16 }
17 }
18 }
有关状压DP的更多相关文章
- BZOJ 1087: [SCOI2005]互不侵犯King [状压DP]
1087: [SCOI2005]互不侵犯King Time Limit: 10 Sec Memory Limit: 162 MBSubmit: 3336 Solved: 1936[Submit][ ...
- nefu1109 游戏争霸赛(状压dp)
题目链接:http://acm.nefu.edu.cn/JudgeOnline/problemShow.php?problem_id=1109 //我们校赛的一个题,状压dp,还在的人用1表示,被淘汰 ...
- poj3311 TSP经典状压dp(Traveling Saleman Problem)
题目链接:http://poj.org/problem?id=3311 题意:一个人到一些地方送披萨,要求找到一条路径能够遍历每一个城市后返回出发点,并且路径距离最短.最后输出最短距离即可.注意:每一 ...
- [NOIP2016]愤怒的小鸟 D2 T3 状压DP
[NOIP2016]愤怒的小鸟 D2 T3 Description Kiana最近沉迷于一款神奇的游戏无法自拔. 简单来说,这款游戏是在一个平面上进行的. 有一架弹弓位于(0,0)处,每次Kiana可 ...
- 【BZOJ2073】[POI2004]PRZ 状压DP
[BZOJ2073][POI2004]PRZ Description 一只队伍在爬山时碰到了雪崩,他们在逃跑时遇到了一座桥,他们要尽快的过桥. 桥已经很旧了, 所以它不能承受太重的东西. 任何时候队伍 ...
- bzoj3380: [Usaco2004 Open]Cave Cows 1 洞穴里的牛之一(spfa+状压DP)
数据最多14个有宝藏的地方,所以可以想到用状压dp 可以先预处理出每个i到j的路径中最小权值的最大值dis[i][j] 本来想用Floyd写,无奈太弱调不出来..后来改用spfa 然后进行dp,这基本 ...
- HDU 1074 Doing Homework (状压dp)
题意:给你N(<=15)个作业,每个作业有最晚提交时间与需要做的时间,每次只能做一个作业,每个作业超出最晚提交时间一天扣一分 求出扣的最小分数,并输出做作业的顺序.如果有多个最小分数一样的话,则 ...
- 【BZOJ1688】[Usaco2005 Open]Disease Manangement 疾病管理 状压DP
[BZOJ1688][Usaco2005 Open]Disease Manangement 疾病管理 Description Alas! A set of D (1 <= D <= 15) ...
- 【BZOJ1725】[Usaco2006 Nov]Corn Fields牧场的安排 状压DP
[BZOJ1725][Usaco2006 Nov]Corn Fields牧场的安排 Description Farmer John新买了一块长方形的牧场,这块牧场被划分成M列N行(1<=M< ...
- 【BZOJ1087】 [SCOI2005]互不侵犯King 状压DP
经典状压DP. f[i][j][k]=sum(f[i-1][j-cnt[k]][k]); cnt[i]放置情况为i时的国王数量 前I行放置情况为k时国王数量为J #include <iostre ...
随机推荐
- 面试题:给你个id,去拿到name,多叉树遍历
前天面试遇到一个多叉树面试的题目,在这里分享记录一下. 题目:一个树形的数据(如下数据),面试官给你一个id,然后拿到对应的name? 数据结构大概是这个样子 var cityData = [ { i ...
- [护网杯 2018]easy_tornado 1
复现一道关于tornado的题目 首先可以得知此题用的是tornado,基于python的后端框架,多半是ssti注入 有三个文件,首先可得知flag在何处 然后观察hint和url就知道要根据coo ...
- java中线程有什么用?
线程有什么用? 通过引入线程技术,在浏览器中你可以浏览网页的同时,播放动画和声音效果,同时在后台打印一个页面.例如老板可以同时处理工程师,秘书和清洁人员的事,这 就是多线程处理机制.Within th ...
- 【Android开发】View 转 Bitmap
public static Bitmap loadBitmapFromView(View v) { int w = v.getWidth(); int h = v.getHeight(); Bitma ...
- new String比字符串池浪费空间,为什么要用它?
对于下面程序中:ss0 = new String( "hello" );是用new()来新建对象的,存于堆中.每调用一次就会创建一个新的对象.当然从节省空间的角度来讲,肯定不如st ...
- CCF201812-1小明上学
题目背景 小明是汉东省政法大学附属中学的一名学生,他每天都要骑自行车往返于家和学校.为了能尽可能充足地睡眠,他希望能够预计自己上学所需要的时间.他上学需要经过数段道路,相邻两段道路之间设有至多一盏红绿 ...
- Java中的反射以及简单运用(原理+例子)
Java反射 学习内容 1. 为什么要使用反射 2. 反射的概念 3. Java反射加载过程 4. 字节码对象理解 5. 获取字节码对象(.class)的三种方式 6. 反射常用API 8. 反射综合 ...
- 微信小程序如何把接口调用成功的回调函数返回的参数return出去?(promise就可以解决)
举个栗子//获取应用实例 //const app = getApp() //const util = require('../../utils/util.js') //const sign = uti ...
- 网络协议之:socket协议详解之Unix domain Socket
目录 简介 什么是Unix domain Socket 使用socat来创建Unix Domain Sockets 使用ss命令来查看Unix domain Socket 使用nc连接到Unix do ...
- 美团动态线程池实践思路开源项目(DynamicTp),线程池源码解析及通知告警篇
大家好,这篇文章我们来聊下动态线程池开源项目(DynamicTp)的通知告警模块.目前项目提供以下通知告警功能,每一个通知项都可以独立配置是否开启.告警阈值.告警间隔时间.平台等,具体代码请看core ...