当前位置:首页 > 技术 > 正文内容

动态规划核心思想与经典问题求解

访客 技术 2026年6月23日 1

动态规划三要素

解决动态规划问题需遵循以下步骤:

  • 状态定义:明确 dp[i]dp[i][j] 的具体含义
  • 状态转移方程:建立状态之间的递推关系
  • 边界条件与实现:确定初始值,选择递归或迭代方式编码

常见优化策略

  • 状态压缩:减少维度,降低空间复杂度
  • 滚动数组:仅保留必要的历史状态
  • 单调队列/斜率优化:加速状态转移过程
  • 记忆化搜索:避免重复计算

记忆化搜索与递推实现

以经典的兔子繁殖问题为例,其本质为斐波那契数列。两种实现方式对比:

方法一:记忆化递归

#include <iostream>
using namespace std;

const int LIMIT = 100;
long long memo[LIMIT + 5] = {0};

long long solve(int month) {
    if (month <= 2) return month;
    if (memo[month]) return memo[month];
    memo[month] = solve(month - 1) + solve(month - 2);
    return memo[month];
}

int main() {
    int n;
    cin >> n;
    cout << solve(n) << endl;
    return 0;
}

方法二:自底向上递推

long long table[105] = {0};

void iterative_solve() {
    int n;
    cin >> n;
    table[1] = 1;
    table[2] = 2;
    for (int i = 3; i <= n; i++) {
        table[i] = table[i-1] + table[i-2];
    }
    cout << table[n] << endl;
}

大整数处理方案

当数值超出 long long 范围时,需自定义大整数类:

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

class LargeNumber : public vector<int> {
public:
    LargeNumber() { push_back(0); }
    
    LargeNumber(int val) {
        push_back(val);
        normalize();
    }
    
    LargeNumber& operator+=(const LargeNumber& other) {
        for (size_t i = 0; i < other.size(); i++) {
            if (i >= size()) push_back(other[i]);
            else at(i) += other[i];
        }
        normalize();
        return *this;
    }
    
    LargeNumber operator+(const LargeNumber& other) {
        LargeNumber result(*this);
        result += other;
        return result;
    }
    
    void normalize() {
        for (size_t i = 0; i < size(); i++) {
            if (at(i) < 10) continue;
            if (i == size() - 1) push_back(0);
            at(i + 1) += at(i) / 10;
            at(i) %= 10;
        }
    }
};

ostream& operator<<(ostream& out, const LargeNumber& num) {
    for (int i = num.size() - 1; i >= 0; i--) {
        out << num[i];
    }
    return out;
}

LargeNumber fib[105];

void big_integer_version() {
    int n;
    cin >> n;
    fib[1] = LargeNumber(1);
    fib[2] = LargeNumber(2);
    for (int i = 3; i <= n; i++) {
        fib[i] = fib[i-1] + fib[i-2];
    }
    cout << fib[n] << endl;
}

Python 简洁实现

Python 天然支持大整数,无需额外处理:

MAX = 100
cache = [0] * (MAX + 5)

def fib_memo(n):
    if n <= 2:
        return n
    if cache[n]:
        return cache[n]
    cache[n] = fib_memo(n-1) + fib_memo(n-2)
    return cache[n]

# 递推版本
def fib_iter(n):
    if n <= 2:
        return n
    dp = [0] * (MAX + 5)
    dp[1], dp[2] = 1, 2
    for i in range(3, n + 1):
        dp[i] = dp[i-1] + dp[i-2]
    return dp[n]

n = int(input())
print(fib_iter(n))

完全背包问题:钱币组合

问题描述:给定 m 种面值的钱币,求凑成 n 元的方案数。

状态设计ways[i][j] 表示使用前 i 种钱币凑成 j 元的方案数。

状态转移:将方案划分为互斥的两类

  • 不使用第 i 种钱币:ways[i-1][j]
  • 至少使用一枚第 i 种钱币:ways[i][j-value[i]]

转移方程:ways[i][j] = ways[i-1][j] + ways[i][j-value[i]]

C++ 实现

#include <iostream>
using namespace std;

const int MAX_AMOUNT = 10000;
const int MAX_TYPES = 20;

int coin_val[MAX_TYPES + 5];
int ways[MAX_TYPES + 5][MAX_AMOUNT + 5];

void coin_combination() {
    int m, n;
    cin >> m >> n;
    for (int i = 1; i <= m; i++) {
        cin >> coin_val[i];
    }
    
    for (int i = 1; i <= m; i++) {
        ways[i][0] = 1;  // 凑成0元有1种方案(什么都不选)
        for (int j = 1; j <= n; j++) {
            ways[i][j] = ways[i-1][j];  // 不选第i种
            if (j >= coin_val[i]) {
                ways[i][j] += ways[i][j - coin_val[i]];  // 选第i种
            }
            ways[i][j] %= 9973;
        }
    }
    cout << ways[m][n] << endl;
}

Python 实现

MOD = 9973

def count_ways(types, amount, values):
    # 初始化DP表
    dp = [[0] * (amount + 1) for _ in range(types + 1)]
    
    for i in range(1, types + 1):
        dp[i][0] = 1  # 边界条件
        for j in range(1, amount + 1):
            dp[i][j] = dp[i-1][j]  # 不选当前钱币
            if j >= values[i-1]:
                dp[i][j] += dp[i][j - values[i-1]]  # 选当前钱币
            dp[i][j] %= MOD
    
    return dp[types][amount]

if __name__ == '__main__':
    m, n = map(int, input().split())
    coins = list(map(int, input().split()))
    print(count_ways(m, n, coins))

空间优化版本

观察状态转移方程,发现 ways[i][j] 仅依赖于当前行和上一行,可优化为一维数组:

void optimized_version() {
    int m, n;
    cin >> m >> n;
    int val[MAX_TYPES];
    for (int i = 0; i < m; i++) cin >> val[i];
    
    int dp[MAX_AMOUNT + 5] = {0};
    dp[0] = 1;
    
    for (int i = 0; i < m; i++) {
        for (int j = val[i]; j <= n; j++) {
            dp[j] += dp[j - val[i]];
            dp[j] %= 9973;
        }
    }
    cout << dp[n] << endl;
}

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。