zoukankan      html  css  js  c++  java
  • 动态规划&&状压

    一、动态规划

    动态规划,无非就是利用历史记录,来避免我们的重复计算。而这些历史记录,我们得需要一些变量来保存,一般是用一维数组或者二维数组来保存。下面我们先来讲下做动态规划题很重要的三个步骤,

    如果你听不懂,也没关系,下面会有很多例题讲解,估计你就懂了。之所以不配合例题来讲这些步骤,也是为了怕你们脑袋乱了

    第一步骤:定义数组元素的含义,上面说了,我们会用一个数组,来保存历史数组,假设用一维数组 dp[] 吧。这个时候有一个非常非常重要的点,就是规定你这个数组元素的含义,例如你的 dp[i] 是代表什么意思?

    第二步骤:找出数组元素之间的关系式,我觉得动态规划,还是有一点类似于我们高中学习时的归纳法的,当我们要计算 dp[n] 时,是可以利用 dp[n-1],dp[n-2].....dp[1],来推出 dp[n] 的,也就是可以利用历史数据来推出新的元素值,所以我们要找出数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],这个就是他们的关系式了。而这一步,也是最难的一步,后面我会讲几种类型的题来说。

    学过动态规划的可能都经常听到最优子结构,把大的问题拆分成小的问题,说时候,最开始的时候,我是对最优子结构一梦懵逼的。估计你们也听多了,所以这一次,我将换一种形式来讲,不再是各种子问题,各种最优子结构。所以大佬可别喷我再乱讲,因为我说了,这是我自己平时做题的套路。

    第三步骤:找出初始值。学过数学归纳法的都知道,虽然我们知道了数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],我们可以通过 dp[n-1] 和 dp[n-2] 来计算 dp[n],但是,我们得知道初始值啊,例如一直推下去的话,会由 dp[3] = dp[2] + dp[1]。而 dp[2] 和 dp[1] 是不能再分解的了,所以我们必须要能够直接获得 dp[2] 和 dp[1] 的值

    案例一、简单的一维 DP

    问题描述:一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

    (1)、定义数组元素的含义

    按我上面的步骤说的,首先我们来定义 dp[i] 的含义,我们的问题是要求青蛙跳上 n 级的台阶总共由多少种跳法,那我们就定义 dp[i] 的含义为:跳上一个 i 级的台阶总共有 dp[i] 种跳法。这样,如果我们能够算出 dp[n],不就是我们要求的答案吗?所以第一步定义完成。

    (2)、找出数组元素间的关系式

    我们的目的是要求 dp[n],动态规划的题,如你们经常听说的那样,就是把一个规模比较大的问题分成几个规模比较小的问题,然后由小的问题推导出大的问题。也就是说,dp[n] 的规模为 n,比它规模小的是 n-1, n-2, n-3.... 也就是说,dp[n] 一定会和 dp[n-1], dp[n-2]....存在某种关系的。我们要找出他们的关系。

    那么问题来了,怎么找?

    这个怎么找,是最核心最难的一个,我们必须回到问题本身来了,来寻找他们的关系式,dp[n] 究竟会等于什么呢?

    对于这道题,由于情况可以选择跳一级,也可以选择跳两级,所以青蛙到达第 n 级的台阶有两种方式

    一种是从第 n-1 级跳上来

    一种是从第 n-2 级跳上来

    由于我们是要算所有可能的跳法的,所以有 dp[n] = dp[n-1] + dp[n-2]。

    (3)、找出初始条件

    当 n = 1 时,dp[1] = dp[0] + dp[-1],而我们是数组是不允许下标为负数的,所以对于 dp[1],我们必须要直接给出它的数值,相当于初始值,显然,dp[1] = 1。一样,dp[0] = 0.(因为 0 个台阶,那肯定是 0 种跳法了)。于是得出初始值:

    dp[0] = 0. dp[1] = 1. 即 n <= 1 时,dp[n] = n.

    三个步骤都做出来了,那么我们就来写代码吧,代码会详细注释滴。

    int f( int n ){
        if(n <= 1)
        return n;
        // 先创建一个数组来保存历史数据
        int[] dp = new int[n+1];
        // 给出初始值
        dp[0] = 0;
        dp[1] = 1;
        // 通过关系式来计算出 dp[n]
        for(int i = 2; i <= n; i++){
            dp[i] = dp[i-1] + dp[-2];
        }
        // 把最终结果返回
        return dp[n];
    }
    

    (4)、再说初始化

    大家先想以下,你觉得,上面的代码有没有问题?

    答是有问题的,还是错的,错在对初始值的寻找不够严谨,这也是我故意这样弄的,意在告诉你们,关于初始值的严谨性。例如对于上面的题,当 n = 2 时,dp[2] = dp[1] + dp[0] = 1。这显然是错误的,你可以模拟一下,应该是 dp[2] = 2。

    也就是说,在寻找初始值的时候,一定要注意不要找漏了,dp[2] 也算是一个初始值,不能通过公式计算得出。有人可能会说,我想不到怎么办?这个很好办,多做几道题就可以了。

    下面我再列举三道不同的例题,并且,再在未来的文章中,我也会持续按照这个步骤,给大家找几道有难度且类型不同的题。下面这几道例题,不会讲的特性详细哈。实际上 ,上面的一维数组是可以把空间优化成更小的,不过我们现在先不讲优化的事,下面的题也是,不讲优化版本。

    摘自

    二、状态压缩

    状态压缩动态规划,就是我们俗称的状压DP,是利用计算机二进制的性质来描述状态的一种DP方式

    在求解背包问题时,我们的状态通常定义为n件物品分别放与不放。 最容易想到的是开个n维数组,但是这样控件浪费且难以实现,我们仔细观察就会发现,每件物品有放与不放两种选择;假设我们有5件物品的时候,用1和0代表放和不放。 如果这5件物品都不放的话,那就是00000; 如果这5件物品都放的话,那就是11111;看到这,我们知道可以用二进制表示所有物品的放与不放的情况;如果这些二进制用十进制表示的话就只有一个维度了。而且这一个维度能表示所有物品放与不放的情况;

    ​ 这个过程就叫做状态压缩;在状态难以表示并且只有决策情况的时候可以用状态压缩。状压其实是一种很暴力的算法,因为他需要遍历每个状态,所以将会出现2^n的情况数量 ,但是求解的规模n一般不会很大。有了状态,我们就需要对状态进行操作或访问

    ​ 可是问题来了:我们没法对一个十进制下的信息访问其内部存储的二进制信息,怎么办呢?别忘了,操作系统是二进制的,编译器中同样存在一种运算符:位运算 能帮你解决这个问题。

    ​ .判断一个数字x二进制下第i位是不是等于1。

    方法:if(((1<<(i−1))&x)>0)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))x=x|(1<<(i−1))

    证明方法与1类似,此处不再重复证明。

    3.把一个数字二进制下最靠右的第一个1去掉。

    方法:x=x&(x−1)

    UVA1099

    题目:给定x*y大小的巧克力,要求只能横着切和纵着切,而且必须一次切到底,即每次切割能把一块巧克力切成两块矩形巧克力 ,问是否能经过若干次操作,把巧克力切成n块,每块面积分别为a1,a2……an;

    思路:每次切一刀(可以横着或者竖着)可以把一切巧克力切成两块小的矩形巧克力,我们可以定义状态:dp[x] [y] [s]为给定长宽为xy的巧克力,能否切出面积集合s。用2进制来表示面积集合,假设切为4块,每块为6,3,2,1,则用1111表示该集合,集合从1-1111一共 2的n+1次方-1,为15个集合。因为s的总面积总是和x*y相等的,不同的话肯定切不了,可以用sum[s]保存每个集合的面积,则y=sum[s]/x,可以规定x为巧克力较短的边,可以把状态dp[x] [y] [s]改为dp[x] [s]。

    题目要求dp[x] [s],我们可以跟x平行切

    把巧克力切成两块,只要切成的两小块,可以满足切成子集,那么整个大块就也能切,则状态转移为求dp[min(x,sum[s0]/x)] [s0]&&dp[min(x,sum[s1]/x)] [s1]

    同时也可以把最短边x切断,

    此时状态转移为dp[min(y,sum[s0]/y)] [s0]&&dp[min(y,sum[s1]/y)] [s1]

    只要两种切法满足其中一个就可以。转移方程找到了题目就很容易了。

    #define _CRT_SECURE_NO_WARNINGS
    #include <iostream>
    #include <stdio.h>
    #include <cstring>
    #include <algorithm>
    #include <queue>
    #include <map>
    using namespace std;
    const int MAXN = 16;
    int n, x, y;
    int a[MAXN];
    int sum[1 << MAXN];
    int dp[101][1 << MAXN]; // 1 为可以 0为不可以 -1为未确定
    
    //计算集合中有几个元素
    int bitcount(int a)
    {
    	int rt = 0;
    	while (a)
    	{
    		//依次取最后一位&1,算是否有元素
    		if (a & 1) rt++;
    		a >>= 1;
    	}
    	return rt;
    }
    
    //求短边x下能否切出集合S
    int dfs(int x, int s)
    {
    	if (dp[x][s] != -1)return dp[x][s]; //若dp中已经计算过,则直接返回结果
    	if (bitcount(s) == 1)return dp[x][s] = 1; //集合中只有一个元素,则不需要再切 
    	int y = sum[s] / x; //计算另一边
    	for (int s0 = (s - 1) & s; s0; s0 = (s0 - 1) & s) //分别给切成的两块不同的集合
    	{
    		int s1 = s ^ s0;
    		if (sum[s0] % x == 0 && dfs(min(x, sum[s0] / x), s0) && dfs(min(x, sum[s1] / x), s1))
    			return dp[x][s] = 1;
    		if (sum[s0] % y == 0 && dfs(min(y, sum[s0] / y), s0) && dfs(min(y, sum[s1] / y), s1))
    			return dp[x][s] = 1;
    	}
    	return dp[x][s] = 0;
    }
    int main()
    {
    	int cs = 1;
    	while (scanf("%d", &n) != EOF)
    	{
    		if (n == 0)break;
    		scanf("%d%d", &x, &y);
    		int ct = 0;
    		for (int i = 0; i < n; i++)
    		{
    			scanf("%d", &a[i]);
    			ct += a[i];
    		}
    		if (ct != x * y || ct % x != 0)
    		{
    			printf("Case %d: No
    ", cs++); continue;
    		}
    
    		//all表示原始的集合 
    			//如果n=3  1<<3-1=111
    			//如果n=4  1<<4-1=1111
    		int ALL = (1 << n) - 1;
    
    		//计算每个集合的面积
    			//			1236
    			//1 2 3 6 =>1111 sum[15]=12
    			//1       =>1000 sum[8]=1
    			//23      =>0110 sum[6]=6
    		for (int s = 0; s <= ALL; s++)
    		{
    			sum[s] = 0;
    			for (int i = 0; i < n; i++)
    			{
    				if (s & (1 << i))
    				{
    					sum[s] += a[i];
    				}
    			}
    		}
    
    		memset(dp, -1, sizeof dp);
    		if (dfs(min(x, y), ALL) == 1)
    		{
    			printf("Case %d: Yes
    ", cs++);
    		}
    		else {
    			printf("Case %d: No
    ", cs++);
    		}
    	}
    	return 0;
    }
    
  • 相关阅读:
    @bzoj
    @bzoj
    @codeforces
    @codeforces
    @bzoj
    @codeforces
    @codeforces
    @codeforces
    @NOIP2018
    反转字符串--C和Python
  • 原文地址:https://www.cnblogs.com/sclu/p/11988533.html
Copyright © 2011-2022 走看看