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

动态规划算法专题:从基础背包到斜率优化

访客 技术 2026年10月1日 6

1. 0/1 背包问题的贪心预处理应用

在处理有限预算的购买问题时,若要求余额尽可能小,且最后一次购买不受预算限制(只要余额不少于 5 元),可以将问题转化为 0/1 背包。核心思路是保留 5 元用于购买价格最高的物品,其余 $m-5$ 的金额则作为背包容量,对前 $n-1$ 个较便宜的物品进行最大化填充。

#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>

using namespace std;

int solve_knapsack() {
    int n, budget;
    while (cin >> n && n != 0) {
        vector<int> prices(n);
        for (int i = 0; i < n; ++i) cin >> prices[i];
        sort(prices.begin(), prices.end());
        cin >> budget;

        if (budget < 5) {
            cout << budget << endl;
            continue;
        }

        int limit = budget - 5;
        vector<int> dp(limit + 1, 0);
        
        // 使用前 n-1 个物品填充 limit 容量
        for (int i = 0; i < n - 1; ++i) {
            for (int j = limit; j >= prices[i]; --j) {
                dp[j] = max(dp[j], dp[j - prices[i]] + prices[i]);
            }
        }
        // 最终余额 = 总预算 - 已选物品总价 - 最大单价物品
        cout << budget - dp[limit] - prices[n - 1] << endl;
    }
    return 0;
}

2. 区间 DP:最小代价构造回文串

给定一个字符串,通过添加或删除字符将其变为回文串。由于增加一个字符和删除一个字符在效果上是等价的,我们只需为每个字符保留 min(add_cost, delete_cost)。定义 $dp[i][j]$ 为将区间 $[i, j]$ 变为回文串的最小代价。

状态转移方程:

  • 若 $s[i] == s[j]$,则 $dp[i][j] = dp[i+1][j-1]$
  • 否则,$dp[i][j] = \min(dp[i+1][j] + cost[s[i]], dp[i][j-1] + cost[s[j]])$
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>

using namespace std;

int memo[2005][2005];
int char_cost[26];

int main() {
    int n, m;
    string s;
    cin >> n >> m >> s;
    for (int i = 0; i < n; ++i) {
        char c;
        int a, d;
        cin >> c >> a >> d;
        char_cost[c - 'a'] = min(a, d);
    }

    for (int len = 2; len <= m; ++len) {
        for (int i = 0; i <= m - len; ++i) {
            int j = i + len - 1;
            if (s[i] == s[j]) {
                memo[i][j] = (len == 2) ? 0 : memo[i + 1][j - 1];
            } else {
                memo[i][j] = min(memo[i + 1][j] + char_cost[s[i] - 'a'],
                                  memo[i][j - 1] + char_cost[s[j] - 'a']);
            }
        }
    }
    cout << memo[0][m - 1] << endl;
    return 0;
}

3. 状压 DP:多任务调度优化

在任务具有截止时间和扣分惩罚时,求最小扣分。当任务数量较少($N \le 15$)时,可使用状态压缩 DP。$dp[mask]$ 表示完成集合 $mask$ 中任务的最小惩罚。为了满足字典序要求,在状态转移时逆序遍历任务。

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

using namespace std;

struct Task {
    string title;
    int deadline, duration;
};

void solve() {
    int n;
    cin >> n;
    vector<Task> tasks(n);
    for (int i = 0; i < n; ++i) cin >> tasks[i].title >> tasks[i].deadline >> tasks[i].duration;

    int total_states = 1 << n;
    vector<int> dp(total_states, 1e9);
    vector<int> time_sum(total_states, 0);
    vector<int> parent(total_states, 0);
    vector<int> last_task(total_states, 0);

    dp[0] = 0;
    for (int mask = 0; mask < total_states; ++mask) {
        for (int i = 0; i < n; ++i) {
            if (!(mask & (1 << i))) {
                int next_mask = mask | (1 << i);
                time_sum[next_mask] = time_sum[mask] + tasks[i].duration;
                int penalty = max(0, time_sum[next_mask] - tasks[i].deadline);
                if (dp[mask] + penalty <= dp[next_mask]) {
                    dp[next_mask] = dp[mask] + penalty;
                    last_task[next_mask] = i;
                    parent[next_mask] = mask;
                }
            }
        }
    }

    cout << dp[total_states - 1] << endl;
    vector<string> res;
    int curr = total_states - 1;
    while (curr > 0) {
        res.push_back(tasks[last_task[curr]].title);
        curr = parent[curr];
    }
    for (int i = n - 1; i >= 0; --i) cout << res[i] << endl;
}

int main() {
    int t;
    cin >> t;
    while (t--) solve();
    return 0;
}

4. 斜率优化:序列分割代价最小化

对于序列分割问题,代价函数包含前缀和的平方项时,通常可以使用斜率优化将 $O(N^2)$ 的复杂度降至 $O(N)$。 方程形式:$dp[i] = \min(dp[j] + (sum[i] - sum[j])^2 + M)$。 展开并整理得:$dp[j] + sum[j]^2 = 2 \cdot sum[i] \cdot sum[j] + dp[i] - M - sum[i]^2$。 这符合直线方程 $y = kx + b$,其中 $y = dp[j] + sum[j]^2$,$x = sum[j]$,$k = 2 \cdot sum[i]$。

#include <iostream>
#include <vector>
#include <deque>

using namespace std;

typedef long long ll;

ll get_y(int j, const vector<ll>& dp, const vector<ll>& s) {
    return dp[j] + s[j] * s[j];
}

void compute() {
    int n;
    ll m;
    while (cin >> n >> m) {
        vector<ll> s(n + 1, 0);
        for (int i = 1; i <= n; ++i) {
            ll val; cin >> val;
            s[i] = s[i - 1] + val;
        }

        vector<ll> dp(n + 1, 0);
        deque<int> q;
        q.push_back(0);

        for (int i = 1; i <= n; ++i) {
            while (q.size() >= 2) {
                int j1 = q[0], j2 = q[1];
                if (get_y(j2, dp, s) - get_y(j1, dp, s) <= 2 * s[i] * (s[j2] - s[j1])) {
                    q.pop_front();
                } else break;
            }

            int best_j = q.front();
            dp[i] = dp[best_j] + (s[i] - s[best_j]) * (s[i] - s[best_j]) + m;

            while (q.size() >= 2) {
                int j2 = q[q.size() - 2], j3 = q.back();
                if ((get_y(j3, dp, s) - get_y(j2, dp, s)) * (s[i] - s[j3]) >= 
                    (get_y(i, dp, s) - get_y(j3, dp, s)) * (s[j3] - s[j2])) {
                    q.pop_back();
                } else break;
            }
            q.push_back(i);
        }
        cout << dp[n] << endl;
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(NULL);
    compute();
    return 0;
}

相关文章

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...

发表评论

访客

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