动态规划技巧专题

一、0,1背包问题

一、问题描述:

有n 个物品,它们有各自的重量和价值,现有给定容量的背包,如何让背包里装入的物品具有最大的价值总和?

解题思路:对于每一个转态,只有选择和不选择两中选择。

记忆化搜索

int n,capacity;
int volume[100],need[100];
int cache[1001][1001];
string name[100];

//背包剩余空间为capacity 时,将返回放入item以后的物品
//所能达到的最大值
int pack(int capacity,int item){
    if(item == n)return 0;
    int &ret = cache[capacity][item];
    if(ret != -1)return ret;
    //不放入物品的情况
    ret = pack(capacity,item+1);
    //不放入物品的情况
    if(capacity>=volume[item])
    ret = max(ret,pack(capacity-volume[item],item+1)+need[item]);
    return ret;
}

迭代法

dp[i][j] 的含义是在j容量下放入i件物品的最大价值。在遍历容量的时候记得是从后向前遍历的,否则结果不会有变化。

#include <iostream>
 
using namespace std;
 
int w[105], val[105];
int dp[105][1005];
 
int main()
{
    int t, m, res=-1;
    cin >> t >> m;
    for(int i=1; i<=m; i++)
        cin >> w[i] >> val[i];
    
    for(int i=1; i<=m; i++) //物品 
        for(int j=t; j>=0; j--) //容量, 
        {
            if(j >= w[i])
                dp[i][j] = max(dp[i-1][j-w[i]]+val[i], dp[i-1][j]);
            else      //只是为了好理解
                dp[i][j] = dp[i-1][j];           
        }
    cout << dp[m][t] << endl;
    return 0;
}

0,1背包问题(数据大)

有n个重量和价值分别为wi, vi的物品,从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值的最大值
与前面唯一不同的地方便是W达到了10 ^ 9、wi到达1了10 ^ 7;

变换思路:定义dp[i+1][j]为前i个物品中挑选出价值总和为j时总重量的最小值(不存在时就是一个充分大的数INF)。
因为前0个物品什么也选择不了、故
dp[0][0] = 0;
dp[0][j]=INF;

此外 前i个物品挑选出价值总和为j时,一定有
前i-1个物品中挑选价值总和为j的部分
前i-1个物品中挑选价值总和为j-v[i]的部分,然后在选中第i个物品

于是又递推式dp[i+1][j] = min(dp[i][j], dp[i][j-v[i]] + w[i])
最终的答案就对应于dp[n][j] <= W的最大的j;

 #include<cstdio>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<map>
#define max(a,b)(a>b?a:b)
#define min(a,b)(a<b?a:b)
typedef long long ll;
using namespace std;
#define N 10010
ll dp[N],w[N];  ///此时的dp[i]表示的是;价值为i时的最小容量为dp[i];
int v[N];
 
int main()
{
    int T,i,n,sum;
    ll V;
        scanf("%d%I64d",&n,&V);
        sum=0;
        for(i=1;i<=n;i++)
        {
            scanf("%I64d%d",&v[i],&w[i]);
            sum=sum+v[i];
        }
 
        memset(dp,1000000010,sizeof(dp)); ///要求最小容量,初始化为最大值;
        dp[0]=0;
        for(i=1;i<=n;i++)
        {
            for(int j=sum;j>=v[i];j--)
                dp[j]=min(dp[j],dp[j-v[i]]+w[i]);
        }
 
        for(i=sum;i>=0;i--)
        {
            if(dp[i]<=V)
            {
               printf("%d\n",i); ///此处输出i,即为满足条件的最大价值
               break;
            }
        }
    return 0;
}

计算第K个答案—摩尔斯电码字典()

题目
摩尔斯电码字典
在没有电话的时代,摩尔斯电码是无线电传输领域中的一种常用代码。电码以短信号(短点,o)和长信号(长点,-)的不同组合表示各种文字。例如:o—表示英文字母J,而—表示英文字母M。
假设有一本以n个长点和m(n、m<=100)个短点组成的、包含所有信号的字典。例如:n=m=2,就会包含如下信号。
–oo
-o-o
-oo-
o–o
o-o-
oo–
这些信号已按照字典顺序排列好了。-的ASKII码是45,而o的ASCII码是111。因此,按照字典顺序,-在前,o在后。给定n和m时,编写代码计算出此字典的第k(k<=1,000,000,000,000)个信号。例如:上述字典的第四个信号是o–o。

DFS 求出生成的所有信号

// s 已经生成的信号
//n:还需要的 - 的个数
// m:还需要的 o 的个数

void generate(int n,int m,string s){
    if(n == 0|| m == 0)
    {
        cout<<s<<endl;
        return ;
    }
    
    if(n>0)generate(n-1,m,s+"-");
    if(m>0)generate(n,m-1,s+"o");
    
}

利用动态规划求解

解题思路:首先要了解把n个长点和m个端点连到s后面,可以用二项式系数表示这种组合的个数,即 ( C n + m m ) (C^m_{n+m}) (Cn+mm)
第一个字符是长点的信号会有多少个呢?第一个是长点或,还有n-1个长点和m个短点。因此有 C n + m − 1 n − 1 个 C^{n-1}_{n+m-1}个 Cn+m1n1

const int M = 1e9+100;
int bino[201][201];
//预先计算出所以必要的二项式系数
//杨辉三角 方法计算
void calcBino(){
    memset(bino,0,sizeof(bino));
    for(int i=0;i<=200;++i){
        bino[i][0] = bino[i][i] = 1;
        bino[i][j] = min(M,bino[i-1][j-1]+bino[i-1][j]);
    }
}

//返回n个-和m个o组成的信号,跳过skip个信号后
//生成的信号
string kth(int n,int m,int skip){
    //n == 0 ,剩余的部分只能是o
    if(n == 0)return string(m,"o");
    if(skip<bino[n+m-1][n-1])
    return "-"+kth(n-1,m,skip);
    return "o" + kth(n,m-1,skip-bino[n+m-1][n-1]);
}

龙曲线

龙曲线介绍:

龙曲线是以简单的数学规则画出一种曲线,它具有以下形态。曲线从一个简单的线段起始,按照一定规则变换此线段完成整个曲线。每形成一次变换称为“完成了一次变换代”,而每完成一代,曲线会进化到更复杂的形式。像这种“放大其一小部分的形状时,表现出与整个形状极为相似构造的图形”,就是分形。
画出龙曲线的方法暂且就称为龙曲线字符串吧!龙曲线字符串由X、Y、F、+、-组成。
那么,要画出龙曲线就从一个点起始画出如下曲线即可。

F:向前方移动一格并画线。
+:向左旋转90度。
-:向右旋转90度。
X、Y:忽略。
画出第0代龙曲线的字符串是FX。从下一代开始,按照如下方式利用前一代字符串进行字符替换,从而获得当前一代的龙曲线字符串。
X-> X+YF
Y-> FX+Y

根据上面的替换式,就有如下的1、2代龙曲线字符串。
第一代:FX+YF
第二代:FX+YF+FX-YF
我们想要求出第n代龙曲线字符串。不过,考虑到答案有可能很长,所以只想计算出第p个字符起始长度为l个字符的字符串。请编写程序实现这种功能。

输入
第一行输入测试用例的个数C(C<=50)。各测试用例的第一行分别输入3个整数,即龙曲线的世代n(0<=n<=50)、p以及l(1<=p<=1 000 000 000、1<=l<=50)。第n代龙曲线字符串的长度可假设成总是大于等于p+l的数值。
输出
每个测试用例在1行内输出第n代龙曲线字符串的第p个字符开始,输出l个字符。

示例输入
4
0 1 2
1 1 5
2 6 5
42 764853475 30
示例输出

FX
FY+YF
+FX-Y
FX-YF-FX+YF+FX-YF-FX+YF-FX-YF-

算法的详细设计思想:

分别找出字母和“+”“-”号规律,对其思想分别进行函数构造,最后进行整合,龙曲线字符串每一代都是前一代的前半部分,代数只决定了龙曲线字符串长度或者图形的终止位,但此次要求不输出图形只输出字符串规律,也就是说所有代数字符串长度内(例如第n代长度为3*2^n-1),相同位置规律是相同的。
由题目可推得:
第一代:FX+YF
第二代:FX+YF+FX-YF
第三代:FX+YF+FX-YF+FX+YF-FX-YF
由上式可看出每一代都是下一代前半部分,还可推出两个规律,字母规律和正负号规律。
字母规律:每六个一循环,第一,二,四,五位分别为F,X,Y,F.
符号规律:符号位都是3的倍数,并且规律如下。
第一代 +
第二代 + + -
第三代 + + - + + - -
1 2 3 4 5 6 7
由上式可看出,每代符号会继承上一代,并且在它们空隙处按照“+” “-”循环插入,类如,第三代继承第二代的2,4,6位置,在1,3,5,7按照“+”“-”循环插入。由此我们可以推算出在奇数位上1,3,5,7······上的奇数位是“+”,偶数位是“-”。例如7在奇数中排第

DFS 求出进化后的字符串

curve(seed,generations) = 输出初始化字符串进化generations代之后的结果。进化就是对字符中的原字符进行替换。

代码:

void curve(const string &seed,int generations){
    if(generations == 0)
    {
        cout<<seed<<endl;
        return;
    }
    for(int i=0;i<seed.size();++i){
        if(seed[i] =='X')
        curve("X+YF",generations-1);
        else if(seed[i] == 'Y')
        curve("FX-Y",generations-1);
        else
        cout<<seed[i];
    }
}

动态规划求解

因为 已经定义了X和Y 替换的字符有 x L e n g t h ( n ) = x L e n g t h ( n − 1 ) + y L e n g t h ( n − 1 ) + 2 xLength(n) = xLength(n-1) +yLength(n-1)+2 xLength(n)=xLength(n1)+yLength(n1)+2 y L e n g t h ( n ) = x L e n g t h ( n − 1 ) + y L e n g t h ( n − 1 ) + 2 yLength(n) = xLength(n-1) +yLength(n-1)+2 yLength(n)=xLength(n1)+yLength(n1)+2

所以有
L e n g t h ( n ) = 2 + 2 ∗ L e n g t h ( n − 1 ) Length(n) = 2+ 2*Length(n-1) Length(n)=2+2Length(n1)

代码:

const int MAX = 1e9+1;
// Length[i] = 把X或Y替换i次后的长度
int Length[51];

void precalc(){
    Length[0] = 1;
    for(int i=1;i<=50;++i)
    Length[i] = min(MAX,Length[i]*2+2);
}

const string EXPAND_x = "X+YF";
const string EXPAND_Y = "FX=Y";
//返回dragonCurve 进化 generations 代后的第 skip+1 个字符
char expand(const string& dragonCurve,int generations,int skip){
    if(generations == 0){
        assert(skip <dragonCurve.size());
        return dragonCurve[skip];
    }
    for(int i=0;i<dragonCurve.size();++i){
        //字符串扩展时
        if(dragonCurve[i] =='X' || dragonCurve[i] == 'Y'){
            if(skip >= Length[generations])
            skip -= Length[generations];
            else if(dragonCurve[i] == 'X')
            return expand(EXPAND_x,generations-1,skip);
            else 
            return expand(EXPAND_Y,generations-1,skip);
        }else if(skip>0) // +,--符号
        --skip;
        else 
        return dragonCurve[i];
        
    }
    //不扩展但需要跳过
    
}

旅行商问题

问题简介
一个商品推销员要去若干个城市推销商品,该推销员从一个城市出发,需要经过所有城市后,回到出发地。应如何选择行进路线,以使总的行程最短。从图论的角度来看,该问题实质是在一个带权完全无向图中,找一个权值最小的Hamilton回路。由于该问题的可行解是所有顶点的全排列,随着顶点数的增加,会产生组合爆炸,它是一个NP完全问题。

小技巧

//输入值是布尔类型数组
当数组的长度为n时,可能的输入值个数是 2 n 2^n 2n。因此,可以把长度为n的数组解释成长度为n的二进制数。等同于A【】中的A【0】是最低位,A【1】是其上一位,A【n-1】是最高位的二进制。

记忆化搜索

为了适用记忆化的方法、应该把函数的定义修改成“尽可能最小限度地接收已选路径方面的倍息”。共有两处使用已选路径path;完成整个路径时计算整个路径长度,查看是否已经访问过某个城市。那么、把定义修改成如下形式就能少接收信息。

  • 1.计算已访问路径的长度:原先shortest Path(函数返回整个路径长度,将其修改成返回剩余路径的最小长度。修改成这种形式后就不需要知道之前各城市的访问顺序,但需要知道当前位置(已选择路线中的最后一个城市)。
  • 2.查看是否已经访问过某个城市:必须查看是否已经访问过某城市,但访问顺序并不重要。因此,只需传递给函数长度为n的布尔型数组即可。

通过这些修改可定义出如下函数。

shortestPath2(here,visited)=给定当前位置here,以及各个城市是否被访问过的信息,并保存在布尔型数组visited时,从here起始访问剩余城市的子路径中,返回长度最小的路径。.

代码:

int n,dist[MAX][MAX];
double cache[MAX][1<<MAX];//初始化为-1
// here :当前位置
//visited : 表示是否已访问各城市
//从here起访问剩余城市的路径中,返回长度最小的路径。
// 访问剩余所有城市的路径中,返回长度最小的路径
//假设总是从0号城市开始

double shortPath(int here,int visited){
    if(visited == (1<<n)-1)return dist[here][0];
    //制表
    double &ret = cache[here][visited];
    if(ret>=0)return ret;
    ret = INF;
    //尝试所有要访问的下一个城市
    for(int next =0;next<n;++next){
        //已经访问过的城市
        if(visited &(1<<next))continue;
        double cand = dist[here][next] + shortPath(next,visited+(1<<next));
        ret = min(ret,cand);
    }
    return ret;
}

韦布巴津

题意:
鸡蛋的价格要满足一下三个条件

  1. 之前的鸡蛋价格是将新鸡蛋价格e中包含的数字重新排列而得到的结果
  2. 之前的鸡蛋价格必须小于新的鸡蛋价格
  3. 之前的鸡蛋价格能被m整除,

搜索所有可能

前一个位置满足以下三个条件才能使用当前位置的数字。

  1. 没有前一个位置,(i==0)
  2. 前一个位置的数字与当前数字不同
  3. 前一个位置的数字已被使用

代码:

//digits :对e的各位数字进行排序后的结果
string e,digits;
int n,m;
//输出所有以e的各位数字组成的结果
//price :已生成的价格
//taken :各位数字是否已被使用

void generate(string price,bool taken[15]){
    if(price.size() == n){
        if(price <e) // 虽然是字符串,但当位数一样时也可以利用<比较大小
        cout<<price<<endl;
        return ;
    }
    for(int i=0;i<n;++i){
        if(!taken[i]&&(i == 0||digits[i-1]!=digits[i]||taken[i-1])){
            taken[i] = true;
            generate(price + digits[i],taken);
            taken[i] = false;
        }
    }
}

动态规划

是否是整除,如果都能整除,只要余数相同,相减之前的数并不长重要。也就是一个小推论,如果两个数相减能整除m,那么这两个数mod m 的余数是一样的。

int MOD = 1e9+10;
//digits :对e的各位数字进行排序后的结果
string e,digits;
int n,m;
int cache[1<<14][20][2];
//从第一位开始对过去的价格进行逐个相加
//index :当前位置的小标
//taken :已使用位置的集合
//mod : 目前为止生成的价格除以m的余数
//less :目前为止生成的价格如果小于e取1,否则为0

int price(int index,int taken,int mod,int less){
    if(taken == n)
    return (less && mod == 0)?1:0;
    // 制表
    int & ret = cache[taken][mod][less];
    if(ret!=-1)return ret;
    ret = 0;
    for(int next =0;next<n;++next){
        if((taken&(1<<next)) == 0){ // 当前位置没用过
            //过去的价格必须小于新的价格
            if(!less && e[index]<digits[next])
            continue;
            //相同数字只能用一次
            if(next >0&&digits[next-1] == digits[next] &&(taken &(1<<next-1)) == 0)
            continue;
            int nextTaken = taken|(1<<next); // 把taken 中第 next 位置为1
            int nextMod = (mod *10 + digits[next]-'0')%m; 
            int nextLess = less|| e[index] > digits[next];
            ret += price(index+1,nextTaken,nextMod,nextLess);
            ret %= MOD;
        }
    }
    return ret;
}

组合游戏

数字游戏

题目大意:
贤宇和舒夏正在用n个整数排成一行的棋盘做游戏。游戏从贤宇开始,双方轮流进行。每个人轮到自己要走棋的时候,能够做出如下两种选择。

  • 可以拿走棋盘最左或最右的一个数字,被拿走的数字会从棋盘中抹掉。
  • 棋盘中还剩余两个以上的数字时,可以把棋盘最右或最左的两个数字抹掉。
    棋盘上的所有数字都消失后就结束游戏,每个人的分数是自己手中数字之和。两人按照“每相差1分就付给分数高的人100韩元”的方式投注,他们都全力以赴的时候,最终的分数差会是多少呢?
const int EMPTY = -987654321;
int n,board[50];
int cache[50][50];

int play(int left,int right){
    if(left>right)
    return 0;
    int &ret = cache[left][right];
    if(ret != EMPTY)
    return ret;
    //拿走数字时
    ret = max(board[left] - play(left+1,right),board[right]- play(left,right-1));
    //抹掉数字是
    if(right - left +1>= 2){
        ret = max(ret,-play(left+2,right));
        ret = max(ret,-play(left,right-2));
    }
    return ret;
}

方块游戏

题目大意:
镇浩和贤焕厌倦了模拟城市和宝石迷阵游戏,打算利用家里的组合型方块进行一个新的游戏。双方轮流向5×5大小的棋盘里放置组合型方块,这些方块已经组合成L形状的三方块组和一字型的两方块组。放置时,必须与棋盘中的分割线对齐,而且不能重叠。下图是进行游戏时的棋面布局。
如图所示,各个方块组可以翻转或旋转。两名棋手轮流放置方块组,到不能再放置时,最后一个放置的棋手将会取得胜利。编写一个程序,给出一种棋面布局时,使之判断当前走棋的运行有没有取胜的方法。

实现方法:

它利用了第16章介绍的位掩码使代码变得更加简单。传递给play0的数值并不是长度为25的布尔类型数组,而是32位的整数型。要想在看(c、)格子中有无方块,只要访问第y×5+x位并查看其数值就能判断。接下来,利用位梯码就能够轻松判断出当前组合块能否放置到各个格子中。假如想把二方块组横向放置到(0,0)和(0,1)格子当中,那么第0和T位需要变成1,所以用数值表示此方块组占用的位数就是3。代码9-22也采用了这种方法,算法预先按照占用的位数计算出各个组合块能够摆出的布局,之后与当前棋面布局进行位AND运算,由此判断当前组合块能否放到此位置。precalc()会生成组合块的所有可能的组合,然后把所有可能组合占用位数的集合保存到moves[。此时,playO内部会对此结果进行循环检索,与当前的棋面布局进行位AND运算。如果得到的答案是0,就表示可以放置当前组合块.

代码:

vector<int>moves;

inline int cell(int y,int x){
    return 1<<(y*5+x);
}
//预先计算出能够放置到棋盘上的各组合快的位置
void precalc(){
    //计算三方块组成的L形组合快
    for(int y = 0;y<4;++y)
    for(int x = 0;x<4;++x){
        vector<int>cells;
        for(int dy = 0;dy<2;++dy)
        for(int dx = 0;dx<2;++dx)
        cells.push_back(cell(y+dy,x+dx));
        int square = cells[0] + cells[1] + cells[2] + cells[3];
        for(int i=0;i<4;++i)
        moves.push_back(square-cells[i]);
    }
    
    //计算两方块组成的组合快
    for(int i=0;i<5;++i)
    for(int j=0;j<4;++j){
        moves.push_back(cell(i,j)+cell(i,j+1));
        moves.push_back(cell(j,i)+cell(j+1,i));
    }
}

char cache[1<<25];
//当前棋面布局为board 时,返回骑手能否取胜
char play(int board){
    char &ret = cache[board];
    if(ret != -1)return ret;
    ret = 0;
    // 考虑所以走法
    for(int i=0;i<moves.size();++i)
    // 确定此走法能否适用于当前的棋面布局
    if((moves[i]&board) == 0){
        if(!play(board|moves[i])){
            ret = 1;
            break;
        }
    }
    return ret;
}

完全背包问题

和0,1背包很像,不过选取的物品总量上限是无限大的。

C[i] 的含义是 容量为 budget 时所得的最大价值价值
都是从 低到高,不像0,1。容量是从高到低

int n,W;
int dp[N],w[110],v[110];
int main()
{
    while(~scanf("%d%d",&n,&W)){
        mem(dp,0);
        for(int i=0;i<n;++i)
        scanf("%d%d",&v[i],&w[i]);
        int ans ;
        for(int j=1;j<=W;++j){
            ans = 0;
            for(int i=0;i<n;++i)
                if(j>=w[i])
                  dp[j] = max(dp[j] , dp[j-w[i]]+v[i]);
            ans = max(ans,dp[j]);
        }
        printf("%d\n",ans);
    }

    return 0;
}

硬币问题

得到金额w 需要的最小的硬币个数

int n,m;
int dp[N],a[25];
int main()
{
    while(~scanf("%d%d",&n,&m)){
        for(int i=0;i<= n;++i)
        dp[i] = INF;
        for(int i=0;i<m;++i)
        scanf("%d",&a[i]);
        dp[0] = 0;
        for(int i=0;i<m;++i)
        for(int j=1;j<=n;++j)
        if(j>=a[i])
        dp[j] = min(dp[j] , dp[j-a[i]]+1);
        printf("%d\n",dp[n]);
    }

    return 0;
}

得到金额 W 的所有方案个数

dp[i] 的含义是 得到容量 i 有 dp[i] 种方法

dp[0] = 1;
for(int i = 0;i<n;++i)
for(int j=1;j<=amount;++j)
if(j - coins[i] >= 0)
dp[j] = dp[j] + dp[j-coins[i]];
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

落春只在无意间

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值