计算机考研-机试指南, 第七章:动态规划

本文深入探讨了动态规划中的典型问题,包括N阶楼梯上楼、完全错排、最长不递增子序列、最长公共子序列等,通过具体实例讲解了状态定义、状态转移方程及初始化的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

N阶楼梯上楼问题

题意: 一次走两阶或者一阶,问有多少种上楼方式。
题目网址

  • 定义: dp[i]为i阶楼梯方式。目标:dp[n]。
  • 考察最后一步,要么上一阶,要么上二阶。故状态转移方程为dp[n] = dp[n-1] + dp[n-2]
  • 初态dp[1] = 1,dp[2] = 2.

代码:

#include <stdio.h>

const int max_n = 100;
int dp[max_n];
int main(){
    dp[1] = 1;
    dp[2] = 2;
    for(int i=3; i<max_n; i++)
        dp[i] = dp[i-1] + dp[i-2];

    int n;
    while(scanf("%d", &n)!=EOF)
        printf("%d\n", dp[n]);
    return 0;
}

不容易系列之一

题意: 完全错排问题
题目网址

  • dp[i]表示1到i完全错排种类数。目标dp[n].
  • 考察第一个数与其他数。从后面选择一个数(第k个)占第一个的位。此时有两种情况。(1)第一个数占第k个数的位。这时有dp[n-2]种。(2)第一个数不能占第k个位,此时第一个数在错排中可以等价为第k个数,这时有dp[n-1]种。
  • 由于又n-1种选择。故错排的状态转移方程为dp[n] = (n-1) * (dp[n-1] + dp[n-2])
  • 初态dp[2] = 1,dp[3] = 2

代码:

#include <stdio.h>

const int max_n = 22;
typedef long long LL;  //使用long long, 不然溢出
LL dp[max_n];

int main(){
    dp[2] = 1;
    dp[3] = 2;

    for(int i=4; i<max_n; i++)
        dp[i] = (i-1) * (dp[i-1] + dp[i-2]);
    int n;
    while(scanf("%d", &n)!=EOF)
        printf("%lld\n", dp[n]);  //lld输出long long
    return 0;
}

吃糖果

题意: 可抽象为上楼梯问题
题目网址

  • dp[i]表示i块巧克力的吃法数量
  • 考察最后一天吃。要么吃1块,要么吃两块
  • 状态转移方程同上楼梯。

代码:

#include <stdio.h>

const int max_n = 22;
typedef long long LL;  //使用long long, 不然溢出
LL dp[max_n];

int main(){
    dp[2] = 1;
    dp[3] = 2;

    for(int i=4; i<max_n; i++)
        dp[i] = (i-1) * (dp[i-1] + dp[i-2]);
    int n;
    while(scanf("%d", &n)!=EOF)
        printf("%lld\n", dp[n]);  //lld输出long long
    return 0;
}

拦截导弹

题意: 求最长不递增子序列长度
题目网址

  • 定义dp[i]:为以第i个数为结尾的最长不递增子序列长度. 目标求: max{dp[i] | i from 1 to n}
  • 考察第i个数,要么自立门户,要么跟着前i-1个后面任意一个。
  • 转移方程 dp[i] = max{1, dp[j]+1 | j<i && a[j] >= a[i]}
  • 初态 dp[1] = 1

代码:

#include <stdio.h>
#include <iostream> // 此两句为max函数所需
using namespace std;


const int max_n = 27;
const int inf = 0x7fffffff;
int dp[max_n];
int a[max_n];


void init(){
    for(int i=0; i<max_n; i++)
        dp[i] = 1;
}

int main(){
    int n;
    while(scanf("%d", &n)!= EOF){
        for(int i=1; i<=n; i++)
            scanf("%d", &a[i]);

        init();
        for(int i=2; i<=n; i++){
            for(int j=1; j<i; j++){
                if(a[j] >= a[i]){
                    dp[i] = max(dp[i], dp[j]+1);
                }
            }
        }

        int ans = -inf;
        for(int i=1; i<=n; i++)
            ans = max(ans, dp[i]);
        printf("%d\n", ans);
    }
    return 0;
}

合唱队形

题意:合唱队形要成为身高先增后减。问最少踢多少人。
题目网址

  • 定义:dp1[i],从左到右最长递增子序列。dp2[i],从右到左最长递增子序列。求出两个dp的值
  • 对每一个人,假设其在最中间。利用两个dp数组求踢的人。
  • 答案即为最大值.

代码:

#include <stdio.h>
#include <iostream>
using namespace std;

const int max_n = 105;
const int inf = 0x7fffffff;
int dp1[max_n], dp2[max_n], a[max_n];

void init(){
    for(int i=0; i<max_n; i++)
        dp1[i] = dp2[i] = 1;
}

int main(){
    int n;
    while(scanf("%d", &n)!=EOF){
        for(int i=1; i<=n; i++)
            scanf("%d", &a[i]);
        init();

        for(int i=2; i<=n; i++){
            for(int j=1; j<i; j++){
                if(a[j] < a[i])
                    dp1[i] = max(dp1[i], dp1[j] + 1);
            }
        }

        for(int i=n-1; i>=1; i--){
            for(int j=n; j>i; j--){
                if(a[j] < a[i])
                    dp2[i] = max(dp2[i], dp2[j] + 1);
            }
        }

        int ans = inf;
        for(int i=1; i<=n; i++)
            ans = min(ans, n-dp1[i]-dp2[i]+1);
        printf("%d\n", ans);
    }
    return 0;
}

Coincidence

题意:求两个字符串的最长公共子序列长度
题目网址

  • dp[i][j]:s1前i个字符,s2前j个字符的最长公共子序列长度。
  • 状态转移。若s[i-1] == s[j-1],dp[i][j] = dp[i-1][j-1] + 1。否则dp[i][j] = max(dp[i-1][j], dp[i][j-1])
  • 初态dp[0][j] = 0, dp[i][0] = 0.
  • 注意:字符串和dp的下标问题。

代码:

#include <iostream>
using namespace std;

const int max_n = 104;
const int inf = 0x7fffffff;
int dp[max_n][max_n];

void init(){
    for(int i=0; i<max_n; i++){
        dp[i][0] = dp[0][i] = 0;
    }
}

int main(){
    string s1, s2;
    int length1, length2;
    while(cin >> s1 >> s2){
        init();
        length1 = s1.length();
        length2 = s2.length();
        for(int i=1; i<=length1; i++){
            for(int j=1; j<=length2; j++){
                if(s1[i-1] == s2[j-1])
                    dp[i][j] = dp[i-1][j-1] + 1;
                else
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
            }
        }

        cout << dp[length1][length2] << endl;
    }
    return 0;
}

搬寝室

题意:问一组数取k对两两组合,求他们的平方和的最小值
题目网址
有n件物品,要搬走k对,每一对花费(a-b) * (a-b)体力,问所花最少体力。

  • 根据推导得到结论:最优方案种,成对的重量是两两相邻的。
  • 定义dp[i][j]:前i件物品里搬j对的最少体力。目标dp[n][k]
  • 对第i件物品:若不选,则dp[i][j] = dp[i-1][j].若选,则必与第j-1件物品配对,故dp[i][j] = dp[i-2][j] + (a[j] - a[j-1]) * (a[j] - a[j-1]),选最小的一个.
  • 初态dp[i][0] = 0 (选0对需要0体力), 由状态转移方程,需要用到i-2的条件,故还需初始化dp[1][j], 而dp[1][j] (j>=1) = inf (不可能达到的状态,一件物品选大于一对物品)

代码:

#include<iostream>
#include<algorithm>
using namespace std;

const int max_n = 2000 + 5;
typedef long long LL;
const LL inf = 0x7fffffffffffffff;
LL dp[max_n][max_n/2];

int init(){
    for(int i=0; i<max_n; i++){
        for(int j=0; j<max_n/2; j++)
            dp[i][j] = inf;
        dp[i][0] = 0;
    }
}

int main(){
    int n, k;
    LL a[max_n];
    while(cin >> n >> k){
        for(int i=1; i<=n; i++)
            cin >> a[i];
        init();
        sort(a+1, a+n+1);

        LL tmp1, tmp2;
        for(int i=2; i<=n; i++){
            for(int j=1; j<=k; j++){
                tmp1 = tmp2 = inf;
                if(dp[i-1][j] != inf)  //由上一个状态转移,上一个状态必须是可达的
                    tmp1 = dp[i-1][j];
                if(dp[i-2][j-1] != inf)
                    tmp2  = dp[i-2][j-1] + (a[i]-a[i-1])*(a[i]-a[i-1]);
                dp[i][j] = min(tmp1, tmp2);
            }
        }

        cout << dp[n][k] << endl;
    }
    return 0;
}

Greedy Tino

题意: 从一组数中,选出两个集合,两个集合的和相等。问其中一个集合的和的最大值是多少。
题目网址

  • 定义dp[i][j+offset]:前i件物品中左集合和比右集合和大j,两个集合数的总和。目标:dp[n][0+offset] / 2。
  • 对第i件物品,我们可以选择不要、放左集合、放右集合三种情况。
  • 状态转移方程:dp[i][j+offset] = max(dp[i-1][offset], dp[i-1][j+offset+a[i]] + a[i], dp[i-1][j+offset-a[i]+a[i]).
  • 初始状态dp[0][j], j不为0 = -inf(不可达)
  • 也要考察有值为零的数的情况。

代码:

#include <iostream>
using namespace std;

const int max_n = 105;
const int max_w = 4005;
const int offset = 2000;
const int inf = 0x7ffffff;

int dp[max_n][max_w];
void init(){
    for(int i=-2000; i<=2000; i++)
        dp[0][i+offset] = -inf;

    dp[0][0+offset] = 0;
}

int main(){
    int T,n,tmp1, tmp2, tmp3;
    int a[max_n];
    int havezero;
    cin >> T;
    for(int t=1; t<=T; t++){
        cin >> n;
        init();
        havezero = 0;
        for(int i=1; i<=n; i++){
            cin >> a[i];
            if(a[i] == 0)
                havezero = 1;
        }

        for(int i=1; i<=n; i++){
            for(int j=-2000; j<=2000;j++){
                tmp1 = tmp2 = tmp3 = -inf;
                // 不要
                if(dp[i-1][j+offset] != -inf)
                    tmp1 = dp[i-1][j+offset];
                // 放左边, 注意这个边界条件!!!!
                if(j+a[i] <= 2000 && dp[i-1][j+a[i]+offset] !=- inf)
                    tmp2 = dp[i-1][j+a[i]+offset] + a[i];
                // 放右边
                if(j-a[i] >= -2000 && dp[i-1][j-a[i]+offset]!= -inf)
                    tmp3 = dp[i-1][j-a[i]+offset] + a[i];
                dp[i][j+offset] = max(tmp1, max(tmp2, tmp3));
            }
        }
        cout << "Case " << t << ": ";
        if(dp[n][offset] == 0){
            if(!havezero)
                cout << -1 << endl;
            else
                cout << 0 << endl;
        }
        else
            cout << dp[n][offset] / 2<< endl;
    }
    return 0;
}

采药

题目大意:典型的0-1背包问题
题目链接

  • dp[i][j] 前i个物品,在背包容量j时有的最大价值。 目标dp[n][w]
  • 0-1背包问题,对第i件物品,要么选要么不选
  • 转态转移:dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i] + v[i])
  • 可以压缩dp[j] = max(dp[j], dp[j-w[i]] + v[i]), 为反序。
  • 初始化,全部初始为0(在0件物品时,无论背包多大都是0)

代码:

#include <iostream>
using namespace std;

const int max_n = 105;
const int max_t = 1005;

int dp[max_t];
int p[max_n], t[max_n];
void init(){
    for(int i=0; i<max_t; i++)
        dp[i] = 0;
}

int main(){
    int T, N;
    while(cin >> T >> N){
        init();
        for(int i=1; i<=N; i++)
            cin >> t[i] >> p[i];
        for(int i=1; i<=N; i++){
            for(int j=T;j>=t[i]; j--)  //逆序
                dp[j] = max(dp[j], dp[j-t[i]]+ p[i]);
        }

        cout << dp[T] << endl;
    }
    return 0;
}

Piggy-Bank

题目大意:变种完全背包问题,要刚好装满且最小
题目链接

  • dp[i][j]:前i件物品,重量为j时,刚好转满,物品最小价值。目标dp[n][w].
  • 对第i件物品,可以选0件,1件,…, k件。
  • 状态转移:注意0件有dp[i-1][j]转移而来,k>=1的最优值由dp[i][j-w[i]]转移而来。故dp[i][j] = min(dp[i-1][j], dp[i][j-W[i] + P[i])
  • 同样可以压缩。且为正序
  • 初态。dp[0] = 0, dp[j] = inf (不可达, j不等于0, 物品为0时不可能装满容量不为0的背包)。

代码:

#include <iostream>
using namespace std;


const int max_n = 505;
const int max_w = 10005;
const int inf = 0x7fffffff;
int P[max_n], W[max_n];
int dp[max_w];

void init(){
    for(int i=1; i<max_w; i++)
        dp[i] = inf;
    dp[0] = 0;
}

int main(){
    int T;
    cin >> T;
    while(T--){
        int E, F, C;
        cin >> E >> F;
        C = F-E;
        int N;
        cin >> N;
        for(int i=1; i<=N; i++)
            cin >> P[i] >> W[i];
        int tmp1, tmp2;
        init();
        
        for(int i=1; i<=N; i++){
            for(int j=W[i]; j<=C; j++){  //核心,这里正序.
                tmp1 = tmp2 = inf; //对于是否能够转移的问题,最好是写出tmp,加上判断
                if(dp[j] != inf)
                    tmp1 = dp[j];
                if(dp[j-W[i]] != inf)
                    tmp2 = dp[j-W[i]] + P[i];
                dp[j] = min(tmp1, tmp2);
            }
        }

        if(dp[C] == inf)
            cout << "This is impossible." << endl;
        else
            cout << "The minimum amount of money in the piggy-bank is " << dp[C] << "." << endl;

    }
    return 0;
}

珍惜现在,感恩生活

题目大意: 典型的多重背包问题.
题目链接

  • 使用2进制分解将多重背包分解为0-1背包求解.

代码:

#include<iostream>
using namespace std;

const int max_n = 105;
const int max_w = 105;

int dp[max_w];

void init(){
    for(int i=0; i<max_w; i++)
        dp[i] = 0;
}
int main(){
    int C;
    cin >> C;
    int W[max_n * 20], V[max_n * 20];
    while(C--){
        init();
        int w,n;
        cin >> w >> n;
        int p, h, c;
        int cnt = 1;
        for(int i=1; i<=n; i++){
            cin >> p >> h >> c;
            // -------------------------------------------
            int t = 1;
            do{
                W[cnt] = t * p;
                V[cnt++] = t * h;
                c -= t;
                t *= 2;
            }while(c - t > 0);
            W[cnt] = c * p;
            V[cnt++] = c * h;
        }
			// ---------------------------------------------
        for(int i=1; i<=cnt-1; i++){
            for(int j=w; j>=W[i]; j--)
                dp[j] = max(dp[j],dp[j-W[i]] + V[i]);
        }
        cout << dp[w] << endl;
    }
    return 0;
}

总结

通过本章,我基本了解了动态规划中的典型问题。总的来说,动态规划关键是状态的定义,然后根据物理意义去得到状态转移方程,一定要考虑所有的转移情况。最后确定初态。感觉有点像数学归纳法的味道。

知识点

  • 斐波拉契数列的场景
  • 错排公式
  • 最长上升子序列
  • 最长公共子序列
  • 天平类问题
  • 0-1背包
  • 完全背包
  • 多重背包
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值