巧克力分割问题变体
📋 题目描述(完整版)
你有n段连续的巧克力,每一段都有一个口味值(整数)。现在需要将这些巧克力切成连续的k段(即切k-1刀,每刀切在两个相邻块之间)。切完后,每一段的得分定义为该段中不同口味值的种类数(即该子数组中不同整数的个数)。你的目标是最大化这k段得分之和,并返回这个最大值。
注意:必须恰好切成k段,不能多也不能少。
📥 输入格式
- 第一行:两个整数
n和k,分别表示巧克力总段数和要切成的段数。 - 第二行:
n个整数,表示每段巧克力的口味值,用空格分隔。
📤 输出格式
- 一个整数,表示最大可能的总得分。
⚠️ 约束条件(合理假设,方便你设计算法)
1 <= k <= n <= 200(n 较小,O(n^3) 可接受,但你能做得更好)- 口味值范围:
1 <= flavor[i] <= 1000(或更大的整数,但无关紧要)
🧪 测试用例(含预期输出)
示例 1
输入: 4 2 1 2 1 2 输出: 4解释:切成[1,2]和[1,2],两段得分各为 2,总和 4。
示例 2
输入: 5 1 1 2 3 1 4 输出: 4解释:不切,整段不同口味为{1,2,3,4},得分 4。
示例 3
输入: 4 2 1 1 1 1 输出: 2解释:无论怎么切,每段得分都是 1,两段总和 2。
示例 4
输入: 5 3 1 2 3 4 5 输出: 5解释:切成[1,2]、[3,4]、[5],得分 2+2+1=5;或[1]、[2,3]、[4,5]得 1+2+2=5,最大为 5。
示例 5(复杂重叠)
输入: 6 3 1 2 1 3 2 3 输出: 6解释:切成[1,2](得分2)、[1,3,2](得分3)、[3](得分1)?实际切法很多,最大可为 6(例如[1,2,1]得2,[3,2,3]得2,再加一段?其实最优可达到6,可尝试[1,2](2)、[1,3,2](3)、[3](1) 总6,或[1](1)、[2,1,3](3)、[2,3](2) 总6,正确输出6。
示例 6(边界:k = n)
输入: 3 3 1 2 1 输出: 3解释:每段一个元素,每段得分均为1,总和3。
示例 7(边界:k = 1,全相同)
输入: 4 1 5 5 5 5 输出: 1解法
好,这次我把重点放在**“DP 矩阵长什么样、每个格子代表什么、为什么边界这样设”**上,代码写得更直白,每一步都有注释。
一、先建立直观:DP 矩阵是一张“表”
列 j = 段数 0 1 2 3 ... K ┌─────┬─────┬─────┬─────┬─────┐ 行 i 0│ dp00│ dp01│ dp02│ dp03│ ... │ 前 0 个元素 ├─────┼─────┼─────┼─────┼─────┤ 1│ dp10│ dp11│ dp12│ dp13│ ... │ 前 1 个元素 ├─────┼─────┼─────┼─────┼─────┤ 2│ dp20│ dp21│ dp22│ dp23│ ... │ 前 2 个元素 ├─────┼─────┼─────┼─────┼─────┤ 3│ dp30│ dp31│ dp32│ dp33│ ... │ 前 3 个元素 ├─────┼─────┼─────┼─────┼─────┤ ... ├─────┼─────┼─────┼─────┼─────┤ n│ dpn0│ dpn1│ dpn2│ dpn3│ ... │ 前 n 个元素 └─────┴─────┴─────┴─────┴─────┘每个格子dp[i][j]回答一个问题:
“把数组的前
i个元素(flavor[0] ~ flavor[i-1]),恰好切成j段,最多能拿多少分?”
我们要的答案是右下角dp[n][K]。
二、矩阵里哪些格子是“合法”的?
关键约束:前i个元素切成j段,每段至少 1 个元素,所以必须i >= j。
列 j = 段数 0 1 2 3 4 ┌─────┬─────┬─────┬─────┬─────┐ 行 i 0│ 0 │ × │ × │ × │ × │ ├─────┼─────┼─────┼─────┼─────┤ 1│ × │ ✓ │ × │ × │ × │ ├─────┼─────┼─────┼─────┼─────┤ 2│ × │ ✓ │ ✓ │ × │ × │ ├─────┼─────┼─────┼─────┼─────┤ 3│ × │ ✓ │ ✓ │ ✓ │ × │ ├─────┼─────┼─────┼─────┼─────┤ 4│ × │ ✓ │ ✓ │ ✓ │ ✓ │ └─────┴─────┴─────┴─────┴─────┘dp[0][0] = 0:前 0 个元素切 0 段,合法,得分 0。dp[i][0](i > 0):前i个元素切 0 段,不可能,非法。dp[0][j](j > 0):前 0 个元素切j段,不可能,非法。- 对角线及右下区域
i >= j才是合法状态。
非法格子用-1(或负无穷)表示,避免它们被错误地拿去做max。
三、转移的直观含义:枚举“最后一刀的左边”
假设要算dp[i][j]:前i个元素切j段。
想象最后一刀切在t-1和t之间(t是最后一段的起点):
下标: 0 1 2 ... t-1 | t t+1 ... i-1 └──── 前 t 个 ──┘ └── 最后一段 ──┘ 切 j-1 段 得分 distinct(t, i-1)所以:
dp[i][j] = max over t: dp[t][j-1] ← 前面 t 个元素切 j-1 段的最优解 + distinct(t, i-1) ← 最后一段自己的得分t的取值范围:
t >= j-1:前面t个元素要切j-1段,至少需要j-1个元素。t <= i-1:最后一段flavor[t ... i-1]必须非空。
一句话:dp[i][j]就是在所有可能的“最后一刀位置”里,挑一个让“前面最优 + 最后一段得分”最大的。
四、完整代码(带详细注释)
#include<bits/stdc++.h>usingnamespacestd;classSolution{public:intmaxTotalDistinct(vector<int>&flavor,intK){intn=flavor.size();if(K>n)return0;// 段数超过元素数,无法切// ========== 第 1 步:预处理 distinct[l][r] ==========// distinct[l][r] = flavor[l..r] 中不同元素的个数vector<vector<int>>distinct(n,vector<int>(n,0));for(intl=0;l<n;l++){unordered_set<int>seen;for(intr=l;r<n;r++){seen.insert(flavor[r]);distinct[l][r]=seen.size();}}// ========== 第 2 步:开 DP 表 ==========// dp[i][j]:前 i 个元素,恰好切 j 段,最大得分// 非法状态用 -1 表示vector<vector<int>>dp(n+1,vector<int>(K+1,-1));// ========== 第 3 步:初始化 ==========dp[0][0]=0;// 前 0 个元素切 0 段,得分 0(唯一合法的“空”状态)// j = 1:只切一段,就是整段的不同元素数for(inti=1;i<=n;i++){dp[i][1]=distinct[0][i-1];}// ========== 第 4 步:按段数 j 递推 ==========for(intj=2;j<=K;j++){// 段数从 2 到 Kfor(inti=j;i<=n;i++){// 前 i 个元素,i >= j 才合法// 枚举最后一段的起点 tfor(intt=j-1;t<=i-1;t++){// 前面 t 个元素切 j-1 段必须合法if(dp[t][j-1]==-1)continue;intlastSegmentScore=distinct[t][i-1];intcandidate=dp[t][j-1]+lastSegmentScore;if(candidate>dp[i][j]){dp[i][j]=candidate;}}}}returndp[n][K];}};五、循环边界为什么是这样?(逐条对照)
| 循环 | 范围 | 为什么 |
|---|---|---|
j从 2 到 K | 外层 | j=1已初始化;j依赖j-1,必须先算小段数 |
i从j到 n | 中层 | 前i个元素切j段,必须i >= j,否则非法 |
t从j-1到i-1 | 内层 | t >= j-1保证前面能切出j-1段;t <= i-1保证最后一段非空 |
dp[t][j-1] == -1跳过 | 转移内 | 前面状态非法,不能参与max |
六、用[1,2,1,2]、K=2填表走一遍
预处理distinct
distinct[0..3][0..3]: r=0 r=1 r=2 r=3 l=0: 1 2 2 2 l=1: - 1 2 2 l=2: - - 1 2 l=3: - - - 1初始化j=1
dp[0][0] = 0 dp[1][1] = distinct[0][0] = 1 dp[2][1] = distinct[0][1] = 2 dp[3][1] = distinct[0][2] = 2 dp[4][1] = distinct[0][3] = 2j=2,i从 2 到 4
i=2,t从 1 到 1:
t=1:dp[1][1] + distinct[1][1] = 1 + 1 = 2→dp[2][2] = 2
i=3,t从 1 到 2:
t=1:dp[1][1] + distinct[1][2] = 1 + 2 = 3t=2:dp[2][1] + distinct[2][2] = 2 + 1 = 3dp[3][2] = 3
i=4,t从 1 到 3:
t=1:dp[1][1] + distinct[1][3] = 1 + 2 = 3t=2:dp[2][1] + distinct[2][3] = 2 + 2 = 4✅t=3:dp[3][1] + distinct[3][3] = 2 + 1 = 3dp[4][2] = 4
最终表
j=0 j=1 j=2 i=0 [ 0 -1 -1 ] i=1 [ -1 1 -1 ] i=2 [ -1 2 2 ] i=3 [ -1 2 3 ] i=4 [ -1 2 4 ] ← 答案答案dp[4][2] = 4,对应切法[1,2] | [1,2],得分2+2=4。✅
七、几个容易写错的点(自查清单)
dp表大小是(n+1) × (K+1),不是n × K。因为“前 0 个元素”也是一个状态。- 非法状态用
-1标记,转移时跳过,避免-1 + 分数污染max。 i从j开始,不是从1开始,否则会算出“前 1 个元素切 2 段”这种非法状态。t从j-1开始,不是从0开始,否则前面t个元素不够切j-1段。dp[i][1]的初始化用distinct[0][i-1],注意右端是i-1不是i。
八、一句话记住这个 DP
dp[i][j]= 前i个元素切j段的最大分;转移就是枚举“最后一段从t开始”,把“前t个切j-1段的最优”加上“最后一段自己的不同口味数”,取最大。
表是(n+1)×(K+1),只有i >= j的格子合法,答案是右下角。