动态规划&&状压

目录

    • 一、动态规划

      • 案例一、简单的一维 DP
    • 二、状态压缩
    • UVA1099

一、动态规划

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

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

第一步骤:定义数组元素的含义,上面说了,我们会用一个数组,来保存历史数组,假设用一维数组 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\n", 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\n", cs++);
        }
        else {
            printf("Case %d: No\n", cs++);
        }
    }
    return 0;
}

原文地址:https://www.cnblogs.com/sclu/p/11988533.html

时间: 2024-10-09 07:28:07

动态规划&&状压的相关文章

动态规划---状压dp

状压dp,就是把动态规划之中的一个个状态用二进制表示,主要运用位运算. 这里有一道例题:蓝书P639猛兽军团1 直接上代码,注释很详细 #include<cstdio> #include<iostream> #include<cstring> #define N 15 #define M 110 #define MAX 550 using namespace std; /* 见蓝书641页 */ int s[MAX]; // 记录一行可能的状态 int num[MAX]

动态规划——状压、树形

问题 A: 铺砖块 时间限制: 1 Sec  内存限制: 128 MB 题目描述 现有n*m的一块地板,需要用1*2的砖块去铺满,中间不能留有空隙.问这样方案有多少种 输入 输入n,m(1<=n, m<=11) 有多组输入数据,以m=n=0结束 输出 输出铺砖块的方案数 样例输入 1 2 1 3 1 4 2 2 2 3 2 4 2 11 4 11 0 0 样例输出 1 0 1 2 3 5 144 51205 入门的状压题.0表示空着,1表示填了,把它的状态压缩成一维. 先初始化一下放横着的状态

玉米田(状压DP)

题目:P1879 [USACO06NOV]玉米田Corn Fields 参考:状态压缩动态规划 状压DP 农场主John新买了一块长方形的新牧场,这块牧场被划分成M行N列(1 ≤ M ≤ 12; 1 ≤ N ≤ 12),每一格都是一块正方形的土地.John打算在牧场上的某几格里种上美味的草,供他的奶牛们享用. 遗憾的是,有些土地相当贫瘠,不能用来种草.并且,奶牛们喜欢独占一块草地的感觉,于是John不会选择两块相邻的土地,也就是说,没有哪两块草地有公共边. John想知道,如果不考虑草地的总块数

HDU5117 Fluorescent 期望 计数 状压dp 动态规划

原文链接https://www.cnblogs.com/zhouzhendong/p/HDU5117.html 题目传送门 - HDU5117 题意 $T$ 组数据. 给你 $n$ 盏灯 ,$m$ 个开关,每一个开关对应的控制一些灯.所有可以控制某盏灯的开关被按了奇数次,那么这盏灯最终是亮着的,否则是不亮的. 现在每一个开关都可以选择按或者不按.我们称对于所有开关都做出 按或者不按 的一种选择 为一种 方案.一种方案的价值是其最终情况下灯数 $x$ 的三次方,即 $x^3$ . 求所有方案的价值

[bzoj3717][PA2014]Pakowanie_动态规划_状压dp

Pakowanie bzoj-3717 PA-2014 题目大意:给你n个物品m个包,物品有体积包有容量,问装下这些物品最少用几个包. 注释:$1\le n\le 24$,$1\le m\le 100$ 想法:以为是什么超级牛逼的背包dp,结果就是状压dp 状态:f[s]表示装s状态的物品需要多少背包,g[s]表示在f[s]的前提下,最大的背包剩余的容量. 转移:直接判断最后一个能不能装下当前物品,转移即可. 还有就是这个题卡常,只能直接用Lowbit枚举1,不能全枚举,会T... ... 最后

[bzoj1879][Sdoi2009]Bill的挑战_动态规划_状压dp

Bill的挑战 bzoj-1879 Sdoi-2009 题目大意: 注释:$1\le t \le 5$,$1\le m \le 15$,$1\le length \le 50$. 想法: 又是一个看数据范围想做法的题,我们想到状压dp. 看了题解... ...网上给的状态是f[len][s]表示长度为len满足状态s的字符串个数. 光看状态... ...可能算重啊?! 其实... ... 状态:dp[len][s]表示长度为len,能且只能满足状态为s的字符串个数. 转移:我们先预处理出g[i]

FZU 1025 状压dp 摆砖块

云峰菌曾经提到过的黄老师过去讲课时的摆砖块 那时百度了一下题目 想了想并没有想好怎么dp 就扔了 这两天想补动态规划知识 就去FZU做专题 然后又碰到了 就认真的想并且去做了 dp思想都在代码注释里 思想是很好想的..唯一的难点大概是 c++里面没有同或这种东西 得自己写 而我又不怎么会位运算 问了蕾姐半天也没搞懂怎么用~这个取反符号 到最后怒而手写了函数 一开始想的是 init后 输入nm都可以秒出 但是在使用~的路途上 发现至少我的方法 做这个题 不能做到init后随便输入 因为 每行 都有

状压DP初探&#183;总结

2018过农历新年这几天,学了一下状态压缩动态规划,现在先总结一下.   状态压缩其实是一种并没有改变dp本质的优化方法,阶段还是要照分,状态还是老样子,决策依旧要做,转移方程还是得列,最优还是最优,无后还是无后,所以它比较好理解.   状压,顾名思义就是要将一些状压想办法压缩起来(可以压,也可以删).其中这些状态都满足相似性和数量很多.这样才好压而且压得有意义.常见于一般的方格题,网络题等等.   所以一般基础的状压就是将一行的状态压成一个数,这个数的二进制形式反映了这一行的情况.所以位运算可

第一次接触状压DP

状压DP入门及理解 *(另类的暴力)*      一般状态数不多的时候就会开数组,但是有的状态并不好表示,于是,状压DP就产生了.     状压DP应该是分两类的,一类是压缩状态,另一类是舍弃状态.    我感觉初学状压DP难就难在二进制运算的应用,了解二进制运算符就显得十分重要.     所以我们先看下表,如果有不会二进制简单应用的请点击https://blog.csdn.net/sinat_35121480/article/details/53510793(神犇请忽略...) 下面就可以看题