环上断环成链,区间分段 DP:dp[i][j] 记前 i 个数分 j 段的最小/最大乘积,段和取模 10 作权值,复杂度 O(n³m)。
OJ: roj
题目 ID: 3522
难度:普及+/提高-
标签:环形dp区间dp动态规划取模
创建: 2026-10-02 05:19
更新: 2026-10-10 11:30
形式化题目
圆桌上按顺时针摆着
在所有切法中,分别求出乘积的最小值和最大值,各输出一行。
以官方数据实测为准:
样例(
| 目标 | 断开成链 | 切法 | 各段和 | 段和 |
乘积 |
|---|---|---|---|---|---|
| 最小 |
|||||
| 最大 |
注意最大值的切法里第一段和是
暴力解法
思路
环上的分割,最直接的枚举方式是两步:
- 断环成链:环有
个位置可以"剪开"。从不同位置剪开,得到不同的链。 - 枚举切法:一条长度为
的链有 个空隙,从中选 个作为分割点,共 种。每种切法用 算出各段和、取模、连乘。
总的枚举量是
但暴力暴露了关键结构:很多切法共享同一段前缀。例如"前 3 个数已切成 2 段"之后,无论这 2 段当初怎么切,接下来要面对的局面是完全一样的——只取决于"用到第几个数"和"已切几段"这两个量,而与切法历史无关。这正是动态规划要利用的子结构重叠。
样例拆解(
| 切法 | 各段和 | 模 10 | 乘积 |
|---|---|---|---|
这条链上最小是
代码
复杂度
瓶颈
正解
关键观察
- 状态合并:定义
为"前 个数切成 段"的最优乘积(min 和 max 各记一份)。它把 种前缀切法压缩成 2 个数。 - 段权值只依赖端点:第
段若是 ,其权值为 ,用前缀和 得到。 - 环 → 链的代价可以接受:断环成链需要枚举
个剪开位置,每条链做一次 的 DP,总量 , 时约 ,绰绰有余。
思路
状态与转移。设链上前缀和为
其中
为什么 min/max 各记一份就够了? 注意权值
样例 DP 填表(链 pre = [0, 4, 7, 6, 8])。行是"前
| — | ||
观察几个格子的来历:
:只有一种切法 ,权值 ,min = max = 12。 :两个来源—— ,与 。这里正是负数取模的考点: ,于是 成为 max 的来源;若用 C 语言截断取模 ,会得到 ,答案全错。官方数据正是用含负数的序列检验这一点。 :来自 ,即切法 。
断环成链。把数组复制拼接
上面
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll NEG = -(1LL << 62);
const ll INF = 1LL << 62;
int n, m;
int a[55], b[110];
ll pre[110];
ll dp_min[55][10], dp_max[55][10];
// 链 b[0..n-1](pre 为其前缀和)分成 m 段的(最小乘积, 最大乘积)
pair<ll, ll> best_products(int n, int m) {
for (int i = 0; i <= n; i++) {
dp_min[i][0] = (i == 0) ? 1 : INF;
dp_max[i][0] = (i == 0) ? 1 : NEG;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= min(i, m); j++) {
ll best_min = INF, best_max = NEG;
for (int k = j - 1; k < i; k++) {
ll seg = ((pre[i] - pre[k]) % 10 + 10) % 10;
if (dp_min[k][j - 1] < INF) {
ll v = dp_min[k][j - 1] * seg;
if (v < best_min) best_min = v;
}
if (dp_max[k][j - 1] > NEG / 2) {
ll v = dp_max[k][j - 1] * seg;
if (v > best_max) best_max = v;
}
}
dp_min[i][j] = best_min;
dp_max[i][j] = best_max;
}
}
return make_pair(dp_min[n][m], dp_max[n][m]);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 0; i < n; i++) cin >> a[i];
// 环 -> 链:复制一份
for (int i = 0; i < 2 * n; i++) b[i] = a[i % n];
pre[0] = 0;
for (int i = 0; i < 2 * n; i++) pre[i + 1] = pre[i] + b[i];
ll pre_orig[110];
for (int i = 0; i <= 2 * n; i++) pre_orig[i] = pre[i];
ll ans_min = INF, ans_max = NEG;
for (int st = 0; st < n; st++) {
// 该起点的前缀和:pre 换成局部窗口
ll p[110];
p[0] = 0;
for (int i = 0; i <= n; i++) p[i] = pre_orig[st + i] - pre_orig[st];
for (int i = 0; i <= n; i++) pre[i] = p[i];
pair<ll, ll> r = best_products(n, m);
ans_min = min(ans_min, r.first);
ans_max = max(ans_max, r.second);
}
cout << ans_min << "\n" << ans_max << "\n";
return 0;
}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 05:19
# update_at: 2026-10-02 05:19
import sys
NEG = -(10**100) # dp_max 的哨兵:比任何可能的乘积都小(权值 ≤ 9,乘积 ≤ 9^50)
INF = 10**100 # dp_min 的哨兵
def best_products(n: int, pre: list[int], m: int) -> tuple[int, int]:
"""链 b[0..n-1](pre 为其前缀和)分成 m 段的(最小乘积, 最大乘积)。"""
# dp_min[i][j]:前 i 个数分成 j 段的最小乘积;dp_max 同理取最大。
dp_min = [[0] * (m + 1) for _ in range(n + 1)]
dp_max = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(n + 1):
dp_min[i][0] = 1 if i == 0 else INF # 分 0 段只有前 0 个数合法(乘积单位元 1)
dp_max[i][0] = 1 if i == 0 else NEG
for i in range(1, n + 1):
for j in range(1, min(i, m) + 1):
# 枚举第 j 段的终点 i:第 j 段是 a[k+1..i],权值 = 段和 mod 10,乘积本身不取模
candidates_min = [
dp_min[k][j - 1] * ((pre[i] - pre[k]) % 10)
for k in range(j - 1, i)
if dp_min[k][j - 1] < INF # 跳过不合法的哨兵状态
]
candidates_max = [
dp_max[k][j - 1] * ((pre[i] - pre[k]) % 10)
for k in range(j - 1, i)
if dp_max[k][j - 1] > NEG // 2
]
dp_min[i][j] = min(candidates_min) if candidates_min else INF
dp_max[i][j] = max(candidates_max) if candidates_max else NEG
return dp_min[n][m], dp_max[n][m]
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data) # 圆桌上的整数个数
m = next(data) # 要分成的段数
a = [next(data) for _ in range(n)]
# 环 → 链:把数组复制一份接在后面,枚举每个起点 st,取 a[st..st+n-1] 这条链。
b = a + a
pre = [0]
for x in b:
pre.append(pre[-1] + x) # pre[i] - pre[k] = b[k..i-1] 的段和
ans_min = INF
ans_max = NEG
for st in range(n):
lo, hi = best_products(n, pre[st : st + n + 1], m)
ans_min = min(ans_min, lo)
ans_max = max(ans_max, hi)
print(ans_min)
print(ans_max)
if __name__ == "__main__":
solve()实现说明:
b = a + a与前缀和pre完成"环 → 链 +段和"; best_products对一条链做 min/max 两条 DP,用哨兵INF/NEG标记不合法状态并在转移前过滤;- Python 的
%天然是数学取模(-1 % 10 == 9),与官方数据语义一致;若落地成 C++,需写((s % 10) + 10) % 10。
复杂度
- 每条链:状态
个,每个转移 ,共 ; 条链合计 ; 时约 次乘法; - 空间
(每条链复用)。Python 实测 5 组官方数据共 。
总结
- 模型:环上分段统计 → 断环成链 + 分段 DP,是「环形 + 划分型 DP」的标准组合。
- 状态设计:
(前 个数分 段的最优值),转移枚举最后一段的起点,前缀和 取段和。 - 两个易错点:
- 负数取模:段和可能为负,模 10 必须落在
(Python %天然正确,C++ 要((s % 10) + 10) % 10); - 乘积不要提前取模:题目要求的是"模值相乘"的原始乘积,若把乘积也对 10 取模,最大值信息会全部丢失。
- 负数取模:段和可能为负,模 10 必须落在
- 复杂度:
,约 量级,轻松通过。
图示解析
flowchart LR
ring["环 a: 4 3 -1 2"] -- "复制拼接 b = a + a" --> chain["链候选 st = 0..n-1"]
chain -- "每条链" --> dp["区间分段 DP<br/>dp[i][j] = 前 i 个数分 j 段<br/>min / max 各一份"]
dp -- "取全局 min" --> lo["最小乘积"]
dp -- "取全局 max" --> hi["最大乘积"]
流程对应本题三个步骤:断环成链(枚举剪开位置)→ 每条链上以