动态规划

目录

  • 动态规划是什么
  • 背包类型动态规划

动态规划(dynamic programming,DP)

一种解题技术和算法设计方法。

  • 狭义:用子问题递推的方法解决组合最优化问题。
  • 广义:和递推法是同义词。

递推法常常用来解决

  • 计数问题
  • 组合最优化问题

递推三要素

  • 子问题(状态)
  • 递推式(状态转移)
  • 边界条件(边界状态)

有 个东西排成一行,要从中选一些,可以一个也不选。相邻的两个东西不能都选。有多少种方法?

解法:递推

从前 个东西里选一些,相邻两个不能都选,有多少种方法?
这里,。
把答案记作 。

目标:。

边界条件:,。

递推式:,。

例题:构造序列

给你一个长为 的整数序列 。序列 初始为空,我们要通过下列两种操作把 变成 。

  • 批量添加:选定任意非负整数 ,在 末尾添加 个 ,花费 个金币。
  • 连续添加:选定任意非负整数 ,在 末尾添加 个从 开始的连续整数,(即 ),花费 个金币。

求把 变成 最少要花多少个金币。

限制
  • 。

把 变成 最少要花多少金币。
这里 。把答案记作 。

目标:

边界条件:

怎么找递推式?

递推式

考虑把 变成 的最后一步。
有「批量添加」和「连续添加」两个选择,但是添加多少项却有 个选择。
注意到

  • 操作的费用和添加的项数无关
  • ()

所以添加的项数越大越好。

阿弥陀签

又称鬼脚图、爬梯游戏,是一种游戏,也是一种简易决策方法,常被拿作抽签或决定分配组合。

先画几条平行的竖线,再画一些横线,每条横线连接相邻两条竖线。两条横线不能相交。

玩法:

  1. 把画的横线盖住,选一条竖线。
  2. 揭开盖子,从所选竖线的上端往下走,遇到横线就走过去,直到走到某条竖线的下端。

例题:阿弥陀签的数量

有 条长度是 厘米的竖线,要画一些横线做成一个阿弥陀签。每条横线的两端点到竖线上端的距离必须是 ,,……,或 厘米。有多少种画法满足:

  • 从第一条竖线的上端出发最后会走到第 条竖线的下端。

输出答案模 。

限制
样例

位置,一步

用一对整数 表示位置:在第 条竖线上,距离竖线的上端 厘米。
,。

初始位置 ,目标位置 。

设当前位置是 ,称下述动作为一步

  • 向下移动 1 厘米,到 。若 处有横线,就沿着横线走过去。

递推

各行相互独立。从上到下考虑每一行。

:对前 行,有多少种画法使得

  • 从 经过 步到达 。

边界条件:。

转移:

DP 的两种实现方式

  • 后向DP(填表法,pull DP)
  • 前向DP(刷表法,push DP)

在各种结构上使用动态规划

  • 序列
  • 网格
  • 树
  • ……

序列上的动态规划

序列有两种子结构,前缀(用一个参数描述)和区间(用两个参数描述)。

  • 用前缀定义子问题
  • 用区间定义子问题

背包类型动态规划

01背包问题

有 个物品,从 到 编号。物品 的重量是 克,价值是 元。
有一个容量(即承重量)是 克的背包。
今要从 个物品中选一些放进背包,所选物品的重量之和不能超过背包的容量。
求所选物品的总价值可能达到的最大值。

  • 都是整数。

贪心

  • 优先拿价值高的物品
3 3
3 3
1 2
1 2
  • 优先拿单位重量的价值高的物品
3 8
7 15
4 8
4 8

动态规划解法

把 个物品按编号从小到大的顺序排成一行。从物品 开始,决定每个物品选不选。

:从前 个物品中选一些,所选物品的总重不超过 ,所选物品的总价值可能达到的最大值。

问题的答案就是 。

递推式

若 则不能选物品 ,有

若 则能选物品 。考虑选物品 和不选物品 这两种情况,有

边界条件

,

long long f[101][100001];
int main() {
    int N, M;
    cin >> N >> M;
    for (int i = 1; i <= N; i++) {
        int w, v;
        cin >> w >> v;
        for (int j = 0; j < w; j++)
            f[i][j] = f[i - 1][j];
        for (int j = w; j <= M; j++)
            f[i][j] = max(f[i - 1][j], f[i - 1][j - w] + v);
    }
    cout << f[N][M];
}
  • 时间复杂度:
  • 空间复杂度:

节省空间的写法

计算 的第 行()只需要 的第 行()。

long long f[2][100001];
int main() {
    int N, M;
    cin >> N >> M;
    int x = 0, y = 1;
    for (int i = 1; i <= N; i++) {
        int w, v;
        cin >> w >> v;
        for (int j = 0; j < w; j++)
            f[y][j] = f[x][j];
        for (int j = w; j <= M; j++)
            f[y][j] = max(f[x][j], f[x][j - w] + v);
        swap(x, y);
    }
    cout << f[x][M];
}

更节省空间的写法

计算 需要 和 ,这两项都在 的左侧。

如果按 从大到小的顺序计算计算各个 ,换言之,从右往左计算 的第 行,就可以用 覆盖 ,因为算出 以后再也用不到 了。

long long f[100005];
int main() {
  int N, M;
  cin >> N >> M;
  for (int i = 0; i < N; i++) {
    int w, v;
    cin >> w >> v; //来一个物品
    for (int j = M; j >= w; j--) //更新f数组
      f[j] = max(f[j], f[j - w] + v);
  }
  cout << f[M];
}

例题:背包 2

有 个物品。编号 。第 个物品的重量是 ,价值是 。

太郎要从 个物品中选一些放进背包带回家。背包的容量是 ,这意味着所选物品的重量之和不能超过 。

求所选物品的价值之和的最大值。

  • 都是整数。

从前 个物品中选一些,所选物品的总重量不超过 ,所选物品的总价值的最大值。

这里,,,子问题有 个,太多了。

注意到 ,,这意味着全部物品的价值之和不超过 。

从前 个物品中选一些,所选物品的总价值等于 ,所选物品的总重量的最小值。

这里,,。

把这个问题的答案记作 ,若无法做到,令 。

边界条件:,,。

递推式:对 ,有

例题:多边形 [CSP-J 2025]

有 根木棍,第 根木棍的长度是 ,要从中选择一些木棍拼成一个多边形,有多少种选法?

两种方案不同当且仅当选择的木棍的下标集合不同,即存在 (),使得其中一种方案选择了第 根木棍,但另一种方案未选择。

根木棍,长度为 ,能拼成一个多边形当且仅当 且所有木棍的长度之和大于所有木棍的长度的最大值的两倍,即 。

限制

解法

所有木棍的长度之和大于所有木棍的长度的最大值的两倍,即

  • 除了最长木棍之外的木棍的总长度大于最长木棍的长度。

把木棍按长度从小到大排序,以下假设有 。

设所选的编号最大的那一根木棍,也就是最长的那一根,的编号是 ,问题变成

  • 从前 根木棍中选一些,所选木棍的长度之和不小于 ,有多少种选法。

:从前 根木棍中选一些,所选木棍的长度之和不小于 ,有多少种选法。

,

边界条件

递推式

最终答案
。

例题:贪吃

高桥为 Snuke 准备了 道菜,从 到 编号。第 道菜的甜度是 ,咸度是 。

高桥可以把这些菜任意排列。Snuke 会按高桥排好的顺序吃这些菜,吃完道菜之后,如果 Snuke 吃过的菜的甜度之和大于 或者咸度之和大于 ,Snuke 就不能再吃了。

高桥想让 Snuke 吃的菜的数量尽可能多。求 Snuke 最多能吃掉多少道菜。

分析

Snuke 至少吃一道菜。
在吃最后一道菜之前,他吃过的菜的甜度之和不超过 ,咸度之和不超过 。

假设在吃的菜的甜度之和不超过 ,咸度之和不超过 的前提下 Snuke 最多能吃 道菜,那么实际上他最多能吃 个道菜。

我们可以用背包类型的动态规划来求 。

:从前 道菜中选 道吃掉,所吃和菜的甜度之和不超过 ,所吃的菜的咸度之和的最小值。

就是满足 的最大的整数 。

能量石

Duda 收集了 颗能量石作为午饭。他的嘴不大,一次只能吃一颗能量石。Duda 吃完第 颗石头需要 秒。

能量石的能量会衰减。第 颗石头最初含有 单位能量,每秒钟损失 单位能量。当石头的能量衰减到零时就不再衰减。当 Duda 开始吃一块石头时,就立即获得这块石头的全部能量,不论他吃完这块石头需要多长时间。

Duda 通过吃能量石最多能获得多少能量?

有 组测试数据。

加一个限制:不吃能量衰减到零的石头。也就是说,吃每个石头时它的能量都 。

考虑一个解里相邻两块石头 ,先吃石头 后吃石头 。如果改为先吃 后吃 ,那么石头 的能量少损失 单位,而石头 的能量多损失至多 单位。于是我们得到

考虑一个解,设 是其中相邻两块石头, 在 之前。如果 ,那么交换 将得到一个更好的解。如果 ,那么交换 得到的解不会更差。

考虑一个最优解,设 是其中相邻两块石头, 在 之前。那么 。如果 ,那么交换 得到的仍是最优解。

注意到 也就是 。进一步,我们得到

考虑一个最优解,设 是其中两块石头, 在 之前。那么 。如果 ,那么交换 得到的仍是最优解。

设 是把全部石头按 值从大到小排列所得的序列。那么存在一个最优解是 的子序列。

解法:把全部石头按 值从大到小排列,然后用动态规划求出一个最优子序列。

:从(排序后的)前 个石头中选一些吃掉,花的总时间等于 ,获得的总能量的最大值。

,

例题:Goodstuff Deck Builder (Hard)

你在玩一个卡牌游戏。一开始,你有 张卡牌,编号 到 ,你可以用这些卡牌来攻击敌人。你还有 点能量用来打出卡牌。

卡牌 的初始费用是 。打出卡牌 消耗的能量等于它的费用,会对敌人造成 点伤害。每打出一张牌后,剩下的牌的费用乘以 。

一张牌只能用一次。不能让能量变成负数。

在以上规则下,你可以从手牌中任意选择若干张,按任意顺序使用。
计算你能造成的最大总伤害。

  • ,,

如果已经选定了要打出的牌,按什么顺序出牌消耗的能量最少?

按初始费用从高到低的顺序出牌。

把卡牌按初始费用从大到小排序。

:从前 张牌中选 张打出,消耗的能量不超过 ,最多能造成多少伤害?

,,。

状态太多。

优化

“每打出一张牌后,剩下的牌的费用乘以 ”,相当于“每打出一张牌后,剩下的牌的费用不变,玩家的能量减半”。

:从前 张牌中选一些打出,剩余的能量是 ,最多能造成多少伤害?

例题:分成三队

有 个人分成了三队。人编号 ,队编号 。现在,人 属于队 。

每人有一个强度值,人 的强度是 。一个队的强度定义为其成员的强度之和。判断能否通过改变某些人所属的队使得三个队的强度相同。若可能,输出最少要几个人换队。否则输出 。

分析

必要条件 1: 整除 。

令 。注意到 。

必要条件 2:。

:把前 个人分到三个队,1 队的强度是 ,2 队的强度是 ,最少有几个人换队?

,。
若无法做到,令 。

目标:。

边界条件:,, 或 。

递推式:

完全背包问题

有一个容量(即承重量)是 克的背包。有 种物品,从 到 编号。一个第 种物品 的重量是 克,价值是 元。每种物品的数量足够多。

今要选一些物品放进背包,所选物品的重量之和不能超过背包的容量。
求所选物品的总价值可能达到的最大值。

  • 都是整数。

“足够多”的含义

一种物品的数量“足够多”是相对于背包的容量来说的,即背包能装多少就有多少。

第 种物品的数量不少于 就算足够多。

:为从前 种物品里选一些物品放进背包,所选物品的总重量不超过 ,所选物品的总价值可能达到的最大值。

起初的问题的答案就是 。

递推式

若 ,第 种物品一件拿不成,有

若 ,考虑选不选第 种物品。

  • 不选第 种物品,化为子问题 。
  • 选第 种物品。换言之,第 种物品至少选一件,化为子问题

此时有

边界条件

,。

节省空间的写法

算出 以后就再也用不到 了,所以可以用 覆盖 。

long long f[100005];
int main() {
  int N, M;
  cin >> N >> M;
  for (int i = 0; i < N; i++) {
    int w, v;
    cin >> w >> v; //来一种物品
    for (int j = w; j <= M; j++) //更新f数组
      f[j] = max(f[j], f[j - w] + v);
  }
  cout << f[M];
}

例题:纪念品

已知未来 天 种纪念品每天的价格,第 天第 种纪念品的价格是 。
每天,小明可以进行以下两种交易任意多次:

  • 以当日价格买一个纪念品;
  • 以当日价格卖出持有的一个纪念品。

每天卖出纪念品换回的金币可以立即用于购买纪念品。

小明会在第 天卖出所有纪念品,他现在有 枚金币, 天之后他最多有几枚金币?

  • 任意时刻,小明手上的金币数不超过。

思路

「第一天买一个纪念品,第三天卖」相当于
「第一天买,第二天卖,第二天买,第三天卖」。

所有可能的操作都可变换为

  • 第一天:买一些纪念品。
  • 第二天:把第一天买的纪念品全卖出,再买一些纪念品。
  • 第三天:把第二天买的纪念品全卖出,再买一些纪念品。
  • ……
  • 第 天:把第 天买的纪念品全卖出。

第一天买纪念品的目标应是让所买的纪念品在第二天卖出后收益最大。

  • 解 个完全背包问题。

多重背包问题

有一个容量(即承重量)是 克的背包。有 种物品,从 到 编号,第 种物品有 个。一个第 种物品 的重量是 克,价值是 元。

今要选一些物品放进背包,所选物品的重量之和不能超过背包的容量。
求所选物品的总价值可能达到的最大值。

  • 都是整数。

思路一:转化成01背包问题

有 个物品的 01背包问题。

时间复杂度 ,太慢。

思路二:转化成01背包,减少物品数量

把每种物品分为若干组,选物品时只能一组一组选,而不是一个一个选。

例如,某种物品共有10个,可以把他们分成四组,每一组包含的物品数量分别是 。这样划分满足一个良好的性质: 中的每个数目都可以用若干组凑出来。

照此思路,对于每个 ,我们希望能够

  • 把 个第 种物品分成较少的几组,使得 里的每个数量都能被其中的一些组凑出来。

二进制划分

二进制划分是一种整数拆分方法,例子如下

设二进制划分把正整数 分成 个数 。那么 。

根据定义可知, 是使得 ,即 ,的最小的正整数 。

设二进制划分把正整数 分成 个数 。那么,每个 都可表为 中的几项之和。

根据二进位拆分的定义,有

其中 。

设 是正整数,。若 ,用 可以凑出 。若 ,那么可以用 凑出 。

多重背包 二进制分组 01背包

令 。

物品数不超过 。

时间:

代码

int N, M;
long long f[100005];
//来一个重量是w,价值是v的物品,更新f数组
void item(int w, int v) {
    for (int j = M; j >= w; j--)
        f[j] = max(f[j], f[j - w] + v);
}

void items(int a, int w, int v) {
    for (int i = 1; i < a; i *= 2) {
        item(i * w, i * v);
        a -= i;
    }
    item(a * w, a * v);
}

int main() {
  cin >> N >> M;
  for (int i = 0; i < N; i++) {
    int a, w, v;
    cin >> a >> w >> v;
    items(a, w, v);
  }
  cout << f[M];
}

思路三

令 为从前 种物品里选一些物品放进背包,所选物品的总重量不超过 ,所选物品的总价值可能达到的最大值。

起初的问题的答案就是 。

的递推式

枚举第 种物品拿了几个。

注意到 依赖的那些项 的第二个下标两两的差都是 的倍数,换言之,这些下标模 同余。

分批计算 数组的第 行的项

把 按除以 的余数分类。

  • 第 类:
  • 第 类:
  • ……

对于一个确定的余数 ,把 记为 。这样,递推式写成

下标变换

令 ,把上式改写成

滑动窗口最大值

令 ,算 化为算

算 化为算

两相对比可见,多了一项 ,可能会少一项 。

,,

center

代码

int main() {
  int n, m;
  cin >> n >> m;
  vector<long long> f(m + 1);// f[i-1][.]
  for (int i = 1; i <= n; i++) {
    int v, w, a;
    cin >> v >> w >> a;
    for (int r = 0; r < w; r++) {   // r:j除以w的余数
      deque<pair<int, long long>> q;//(t, g(t))
      for (int k = 0, j = r; j <= m; j += w, k++) {
        // 计算 f[i][j] 之前要把 (k, g(k)) 放进队列
        long long g = f[j] - k * v;
        while (!q.empty() && g >= q.back().second)
          q.pop_back();
        q.push_back({k, g});
        if (q[0].first < k - a)
          q.pop_front();
        f[j] = q[0].second + (long long) k * v;
      }
    }
  }
  cout << f[m];
}

例题:硬币问题

有 种硬币,第 种硬币有 个,单个的价值也为 。

从这些硬币中选出一些使得选中硬币的总价值恰好为 ,求有多少个不同方案。
答案模 。

如果对于任意一种硬币,两个方案选中的数量不同,那么算作两种不同方案。

限制

例题:Diversity

商店里有 个商品在售,第 个商品的价格是 元,效用是 ,颜色是 。

你要从这些商品中买一些(可以一个都不买)。所买物品的价格之和不得超过 元。

你的满意度是 ,其中 是所买物品的效用之和, 是所买物品的不同颜色的数量, 是给定的常数。

求最大满意度。

限制
  • 上述值都是整数。

思路一

把 个商品按颜色排列,颜色相同的排一起。

:从前 个商品里选一些,所选商品的总价格不超过 ,满意度的最大值。

考虑两种情况:

  • 不买商品 ,归结为 。
  • 买商品 。设商品 的颜色是 。分两种情况:
    • 商品 是买的第一个颜色为 的商品。设上一种颜色的最后一个商品是第 个,归结为 。
    • 商品 不是买的第一个颜色为 的商品。归结为 。

思路二

把物品按颜色分组。一次处理一批同色的物品。

从颜色是 的物品中选一些,所选物品的总价不超过 ,满意度的最大值?

把答案记作 。这里,,。

目标:。

边界条件:,。

递推式:
对每种颜色 ,

  1. 置 ,。
  2. 对每个颜色是 的物品 ,按 的顺序,置

例题:分组

给你 个整数 。要把它们分成若干组,每一组的极差之和不超过 。求分组方法数,模 。

一组整数的极差是最大值减最小值。

限制
样例
3 2
2 5 3
3

分组方法:

  • (2), (3), (5)
  • (2, 3), (5)
  • (2), (3, 5)

把 个数从大到小排序。

:对前 个数分组,尚未分好的组(最小值尚未出现)有 个,且满足

  • 已分好的组的极差之和 + 未分好的组的最大值之和 ≤

有多少种方法?

注意到

目标:。

边界条件:(),()。

状态转移

例题:两场考试

个人,从 到 编号,参加了国家队选拔考试。
考试有两场。第 个人第一场考试的排名是 ,第二场考试的排名是 。每场考试都没有同分的, 都是 的排列。

要选 个人组国家队,选人规则是:

  • 不能有两人 满足 且 , 入选了而 没入选。

求选人方案数,模 。

限制

思路

把 个人按照第一场考试的排名从小到大(或者说从高到低)排序,按这个顺序写出每个人第二场考试的排名,把这个序列称为 。不难看出 是 的一个排列。

例如,, 那么 。

问题变成

  • 从序列 中选 项,满足:若选了 那么 左边比 小的项都选了。或者说若没选 那么 右边比 大的项都没有选。有多少种选法?

考虑 DP。

:从 中选 个数,没选数的数中最小的是 ,有多少种选法。

从 往后转移:

  • 若 ,那么 不能选,转移到 。
  • 若 ,那么 可选可不选。若选 ,转移到 ,否则转移到 。

例题:Candy

给定整数序列 ,整数 和整数 。我们称交换 里相邻的两项为一次操作。

要使 的前 项之和不小于 ,至少要进行多少次操作?

  • ()

思路

设想我们已经选定了要放到前 个位置的那 个数,它们,在最初的序列 里,是 ()。

要把 这 个数移到前 个位置,最少要操作多少次?

应该把 移动到第 位,把 移动到第 位,……,把 移动到第 位。
一共要操作 次。

:从 中选 个数,把它们移动到前 位需要操作 次,所选的数之和的最大值。

,,。

答案:满足 的最小的 。

边界条件:

()
( 或 )

递推式:

例题:Emiya 家今天的饭 ⭐

Emiya 掌握 种烹饪方法,且会使用 种主要食材做菜。

Emiya 做的每道菜都将使用恰好一种烹饪方法与恰好一种主要食材。Emiya 会做 道不同的使用烹饪方法 和主要食材 的菜(、)。

Emiya 今天要做 道菜,要满足下列条件

  • 每道菜的烹饪方法互不相同
  • 每种主要食材至多在至多在一半的菜(即 道菜)中被使用

求做菜方案数,模 。



思路

  • 令 。
  • 满足条件一、二的做菜方案的数量为 。
  • 考虑满足条件一、二但不满足条件三的做菜方案的数量。
    • 有主要食材在超过一半的菜中被使用。
    • 在超过一半的菜中被使用的主要食材只能有一种。

对于每一种主要食材 (),计算 使用次数超过一半的做菜方案。

:从前 个烹饪方法中选一些来做菜,主要食材是 的菜与主要食材不是 的菜数量之差是 的做菜方案的数量。

,。

递推式

边界条件
,()

例题:愿望清单

商店里有 个商品,从 到 编号。商品 的价格是 元。
高桥想要 个商品:商品 ,商品 ,…,商品 。

他重复下述操作直到买齐他想要的商品。

令 为当前尚未卖出去的商品的数量。选择一个整数 满足 ,买剩余物品中编号第 小的那个物品,花的钱是那个物品的价格再加上 元。

求高桥最少要花多少钱。

高桥也可以买他不想要的物品。

限制

关键性质

  • 编号大于 的商品不影响高桥想买的物品的编号的排位,不用考虑。
    不妨置 。
  • 何时买商品 不影响对前 个商品中任何一个的购买。
  • 设最终在前 个商品中买了 个,如果也买了商品 ,那么买它时,它的编号排名可以是 中的任何一个。所以买商品 的花费是 。

:在前 个商品中买 个,想要的商品的都买了,最少要花多少钱?

这里,,。
状态 要满足

前个物品中必买的物品的数量

才合理。
若状态 不合理,令 。

目标:。

边界条件:()

递推式:对 ,

商品必买商品不必买

例题:翻转卡片 2

有 张卡片,编号 到 。
第 张卡片正面写着整数 ,背面写着整数 。这里,。

对于每个 ,解决下述问题。

张卡片正面向上放在桌面上。你可以把一些(可以是零张)卡片翻转(反转过后,卡片背面向上)。
为了让所有卡片向上那一面上的数字之和等于 ,最少要翻转多少张卡片?
如果无法通过翻转卡片让所有卡片向上那一面上的数字之和等于 ,输出 。

限制

例题:多重集合平均数

给定整数 ,,,对于每个整数 ,解决下述问题。

  • 求满足下列条件的非空多重集合 的个数除以 的余数:
    • 由 构成,每种数不超过 个(可以是 个)。
    • 里的数平均数是 。
限制
  • 是素数。

# [abc375_e](https://atcoder.jp/contests/abc375/tasks/abc375_e) 3 Team Division

(CSP-S 2019 D2T1)

# [abc288_e](https://atcoder.jp/contests/abc288/tasks/abc288_e) Wish List