首页 > 其他分享 >DP 详解

DP 详解

时间:2024-10-27 15:11:41浏览次数:1  
标签:int ll dfs 详解 cases now DP rightarrow

DP 概述

DP(Dynamic programming,全称动态规划),是一种基于分治,将原问题分解为简单子问题求解复杂问题的方法。

动态规划的耗时往往远少于朴素(爆搜)解法。

动态规划 and 递归

之前说过,动态规划也是分治思路,而递归更是传统的分治思路,但时间复杂度却大相径庭,为什么呢?

动态规划是 自顶向上 思想,而递归是 自顶向下 解法。

自顶向上 and 自顶向下?

自顶向上

意思很简单,从下往上推导:\(f(1) \rightarrow f(2) \rightarrow \dots \rightarrow f(n - 1) \rightarrow f(n)\)。

这也是为什么 动态规划算法 脱离了 递归 的函数,改用循环迭代推到的原因。

自顶向下

反过来,自顶向下就是从上往下推,触底后在将结果返回回来。

\(f(n) \rightarrow f(n - 1) \rightarrow \dots \rightarrow f(2) \rightarrow f(1) \rightarrow f(2) \rightarrow f(3) \rightarrow \dots \rightarrow f(n - 1) \rightarrow f(n)\)

这也是为什么递归比动态规划时间复杂度高的多的原因。

我们可以看出,动态规划更像是递推算法的 plus 版。

状态转移方程

状态转移方程,就是如何将子问题转移至父亲问题的公式。

在简单 DP 中,转移方程可以直接套用至 dfs, bfs 等爆搜算法。

DP 最难的部分就是列出状态转移方程,如果没有状态转移方程,一切都白搭。

例:设 \(f_i\) 为数列第 \(i\) 为的数,斐波那契数列的状态转移方程为 \(f_i = f_{i - 1} + f_{i - 2}\)。

DP 如下:

f[1] = 1;
f[2] = 1;
for (int i = 3; i <= n; i++)
    f[i] = f[i - 1] + f[i - 2]; // 转移方程
cout << f[n];

同样的,我们可以将转移方程套用在递归暴力上:

int f(int n)
{
    if (n == 1 || n == 2)
        return 1;
    return f(n - 1) + f(n - 2); // 转移方程
}

动态规划要素

  1. 最优子结构:问题的最优解 包含 子问题最优解。即为:局部最优解 = 全局最优解。

  2. 无后效性

    • 在推导后面状态时,仅在意前面状态数值,不在意是如何推导出来的。

    • 某状态确定后,不会因为后面的决策而改变前面的决策。

  3. 重叠子问题:不同的决策到达相同的状态时可能产生重复的状态,为了避免不必要的计算,我们通常使用 记忆化搜索(在计算出新状态时将它存储起来一遍下次使用)来解决,这也是最经典的 空间换时间

不满足这三点你还想 DP?想 peach 呢?

状态的定义

前言:空间换时间

很简单的名字,即为使用空间的代价来确保不会超时。

状态?

状态,通俗来讲就是你 \(f_{xxx}\) 代表的是什么。比如斐波那契数列中 \(f_i\) 代表的就是第 \(i\) 为是什么。

对于状态:

  1. 状态越多,表示的信息越多,空间越大

  2. 反之,状态越少,表示的信息越少,空间越小

在我们状态定义时,可能有这些情况:

\(部分情况 \begin{cases} 状态太少?\begin{cases} 信息量太少 & 无解 \\\\ 信息量太少 & 不满足动态规划要素 \end{cases} \\\\ 状态太多? \begin{cases} 空间太大 & MLE \\\\ 需要太多时间更新状态 & TLE \end{cases} \end{cases}\)

所以,状态 and 状态转移方程时整个动态规划中最最最难的部分,想清楚这两点,这题也就解出来了。

参考资料

https://zh.wikipedia.org/wiki/动态规划

五大基本算法之动态规划算法 DP dynamic programming

动态规划解题套路框架 | labuladong 的算法笔记

1.最优子结构 | 数据结构与算法之美

例题

例题一思路

纯 DP

点我查看题目

没看数据:好一个 dfs!

注:两种情况

  1. 拿本物品

    • 3 倍奖金?

    • 1 倍奖金?

  2. 不拿本物品

ll dfs(int i, int now, ll cnt)
{
    if (i == n + 1)
        return cnt;
    if (!((now + 1) % 3) && ((now + 1) >= 3))
        return max(dfs(i + 1, now + 1, cnt + (a[i] * 3)), dfs(i + 1, now, cnt));
    else
        return max(dfs(i + 1, now + 1, cnt + a[i]), dfs(i + 1, now, cnt));
}

我们看题面,一眼看出的状态为:\(f_i\) 表示前 \(i\) 个物品获得的最大奖金。

但是,我们发现不满足无后效性。

根据上述方法,我们尝试使用空间的代价来优化。

将状态改为:\(f_{i, j}\) 表示前 \(i\) 个物品,当前物品数取余 \(3\) 为 \(j\) 时获得的最大奖金。

\(f{i, j} = \begin{cases} j = 0 \begin{cases} i \ge 3 \begin{cases} f_{i - 1, 0} & 不拿 \\\\ f_{i - 1, 2} + a_i \times 3 & 拿 \end{cases} \\\\ f_{i - 1, 0} & 没有到 3 个,不存在这种情况。 \end{cases} \\\\ j = 1 \begin{cases} f_{i - 1, 1} & 不拿 \\\\ f_{i - 1, 0} + a[i] & 拿 \end{cases} \\\\ j = 2 \begin{cases} i \ge 2 \begin{cases} f_{i - 1, 2} & 不拿 \\\\ f_{i - 1, 1} + a_i & 拿 \end{cases} \\\\ f_{i - 1, 2} & 没有至少 2 个物品,没有这种情况。 \end{cases} \end{cases}\)

完整代码为:

#include <bits/stdc++.h>
using namespace std;

#define ll long long
int n;
ll a[100005];

/*
            20PTS
ll dfs(int i, int now, ll cnt)
{
    if (i == n + 1)
        return cnt;
    if (!((now + 1) % 3) && ((now + 1) >= 3))
        return max(dfs(i + 1, now + 1, cnt + (a[i] * 3)), dfs(i + 1, now, cnt));
    else
        return max(dfs(i + 1, now + 1, cnt + a[i]), dfs(i + 1, now, cnt));
}
*/

ll f[100005][3];
ll ans;

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    // cout << dfs(1, 0, 0) << "\n";

    for (int i = 1; i <= n; i++)
    {
        f[i][0] = f[i - 1][0];
        f[i][1] = f[i - 1][1];
        f[i][2] = f[i - 1][2];
        if (i >= 3)
            f[i][0] = max(f[i][0], f[i - 1][2] + (a[i] * 3));
        f[i][1] = max(f[i][1], f[i - 1][0] + a[i]);
        if (i >= 2)
            f[i][2] = max(f[i][2], f[i - 1][1] + a[i]);
        ans = max(ans, f[i][0]);
        ans = max(ans, f[i][1]);
        ans = max(ans, f[i][2]);
    }
    cout << ans << "\n";
    return 0;
}

点我查看题目

首先,我们欣赏一下原出题人的提示。

例题二前言:分类讨论

在看了许多不当人的讲解后,我浓缩出:分类讨论就是分类 --> 讨论!分类讨论就是将问题通过不同的结果 / 形式 / 不同点分成几类逐个解决。

例题二思路

既然说到分类讨论我们先来分个类。

\(\max(\sum_{i = 1}^{N} A_i) = \begin{cases} C > 0 & \max(\sum_{i = L}^{R} A_i) \times C \\\\ C < 0 & \min(\sum_{i = L}^{R} A_i) \times C \end{cases}\)

最大最小怎么使用 \(O(N)\) 求?Bingo!最大 / 最小 子段和即可。

最后比一下就好了。

完整 Code:

#include <bits/stdc++.h> 
using namespace std;

#define ll long long
int n;
ll c;
ll a[100005];
ll solve()
{
    ll original_sum = 0;
    for (int i = 1; i <= n; ++i)
        original_sum += a[i];

    ll dp_max[100005], dp_min[100005];
    dp_max[1] = a[1];
    dp_min[1] = a[1];

    ll maxx = dp_max[1];
    ll minn = dp_min[1];

    for (int i = 2; i <= n; i++)
    {
        dp_max[i] = max(a[i], dp_max[i - 1] + a[i]);
        dp_min[i] = min(a[i], dp_min[i - 1] + a[i]);
        maxx = max(maxx, dp_max[i]);
        minn = min(minn, dp_min[i]);
    }

    ll res = max((c - 1) * maxx, (c - 1) * minn);
    ll ans = original_sum + res;
    return ans;
}

int main()
{
    cin >> n >> c;
    for (int i = 1; i <= n; ++i)
        cin >> a[i];
    cout << solve() << endl;
    return 0;
}

标签:int,ll,dfs,详解,cases,now,DP,rightarrow
From: https://www.cnblogs.com/George222/p/18508452

相关文章

  • Nuxt.js 应用中的 imports:sources 事件钩子详解
    title:Nuxt.js应用中的imports:sources事件钩子详解date:2024/10/27updated:2024/10/27author:cmdragonexcerpt:imports:sources是Nuxt.js的一个生命周期钩子,用于在模块设置过程中执行。开发者可以利用这个钩子来扩展模块的源,方便地管理依赖和模块化配置。categ......
  • Java 权限修饰符详解
    Java权限修饰符详解在Java中,**权限修饰符(AccessModifiers)**用于控制类、方法、变量和构造函数的可见性。理解和使用这些修饰符可以帮助我们更好地封装和组织代码,提高程序的安全性和可维护性。1.权限修饰符的类型Java中主要有四种权限修饰符,分别是:public、protecte......
  • Nuxt.js 应用中的 imports:sources 事件钩子详解
    title:Nuxt.js应用中的imports:sources事件钩子详解date:2024/10/27updated:2024/10/27author:cmdragonexcerpt:imports:sources是Nuxt.js的一个生命周期钩子,用于在模块设置过程中执行。开发者可以利用这个钩子来扩展模块的源,方便地管理依赖和模块化配置。......
  • C#线程详解及应用示例
     简介在编写应用程序实现业务功能过程中,为解决吞吐量和响应效率的问题,我们会用到多线程、异步编程两项重要的技术。通过它们来提高应用程序响应和高效。应用程序每次运行都会启动一个进程(进程是一种正在执行的程序),而进程中可以包含一个或多个线程,由应用程序入口直接或间接执......
  • 简答剖析 UDP:从基础代码到高级封装与应用
    C++学习之路一、C++中构造函数与析构函数简单解析二、Makefile编写简单教程三、UDP协议学习四、简答剖析UDP:从基础代码到高级封装与应用简答剖析UDP:从基础代码到高级封装与应用C++学习之路前言一、UDP基础:涉及的API和结构体(一)`sockaddr_in`结构体(二)`sock......
  • 详解 helm 部署 traefik
    安装helm下载地址https://github.com/helm/helm/releases安装wgethttps://get.helm.sh/helm-v3.16.2-linux-amd64.tar.gztar-zxvfhelm-v3.16.2-linux-amd64.tar.gzcdlinux-amd64/chmod755helmmvhelm/usr/local/bin/helmversion添加traefik的helm源helmrep......
  • 详解 helm 部署 ingress-nginx
    使用Helm安装参考文档:https://kubernetes.github.io/ingress-nginx/deploy/添加ingress-nginx官方helm仓库helmrepoaddingress-nginxhttps://kubernetes.github.io/ingress-nginxhelmrepoupdate下载Chart包#查找所有的版本helmsearchrepoingress-nginx/ingress-n......
  • 宝塔安装wordpress出现500错误
    当在宝塔面板安装WordPress时遇到500错误,通常表示服务器端出现了问题。以下是排查和解决500错误的一些常见步骤:1.检查错误日志首先,查看网站的错误日志,以获取更多关于500错误的详细信息。访问宝塔面板:进入宝塔面板,找到你的网站。点击“设置”->“日志”->“查看错误日......
  • 【Nginx学习】Nginx configure详解:生成的文件你都了解吗?
    ......
  • 【C语言】预处理(预编译)详解(上)(C语言最终篇)
    文章目录一、预定义符号二、#define定义常量三.、#define定义宏四、带有副作用的宏参数五、宏替换的规则六、宏和函数的对比1.宏的优势2.函数的优势3.宏和函数的命名约定一、预定义符号  学习本篇文章的内容推荐先去看前面的编译和链接,才能更好地理解和吸收,文章......