核心思想

区间dp:大范围的问题拆分成若干小范围的问题来求解 可能性展开的常见方式:

1)基于两侧端点讨论的可能性展开

2)基于范围上划分点的可能性展开

例题

1、完成配对需要的最少字符数量

题目描述: 给定一个由 '[' ,']','(',‘)’ 组成的字符串,请问最少插入多少个括号就能使这个字符串的所有括号左右配对。

例如当前串是 "([[])",那么插入一个']'即可满足。 数据范围:字符串长度满足 1≤n≤100 1≤n≤100 

输入描述:

仅一行,输入一个字符串,字符串仅由 '[' ,']' ,'(' ,‘)’ 组成 输出描述:

输出最少插入多少个括号 示例1 输入: ([[]) 输出: 1 示例2 输入: ([])[] 输出: 0

解题思路:

  • dp[i][j]: 表示在s[i, j]的范围内区号区间匹配需要插入的括号数有多少

  • 这里主要有2个分类讨论

    • s[i, j]是一组,即类似【(([ ]))】这类

    • s[i, j]其实包含多组符号,即【】(【】)() 这种,即基于每个可能的划分点,做左右划分,那么就可以分为2个部分

代码如下:

 package main.java.class077;  
   
 /**  
  * @program: ZuoChengxunAlgorithmClass  
  * @ClassName Code01_MinimumInsertionsToMatch  
  * @description: 完成配对需要的最少字符数量  
  * // 给定一个由'['、']'、'(',')'组成的字符串  
  * // 请问最少插入多少个括号就能使这个字符串的所有括号正确配对  
  * // 例如当前串是 "([[])",那么插入一个']'即可满足  
  * // 输出最少需要插入多少个字符  
  * // 测试链接 : https://www.nowcoder.com/practice/e391767d80d942d29e6095a935a5b96b  
  * @author: zs宝  
  * @create: 2025-09-15 11:01  
  * @Version 1.0  
  **/public class Code01_MinimumInsertionsToMatch {   
     // 注意类名必须为 Main, 不要有任何 package xxx 信息  
     public class Main {  
         public static void main(String[] args) throws IOException {  
             BufferedReader buffer = new BufferedReader(new InputStreamReader(System.in));  
             PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));  
             String str = buffer.readLine();  
             out.println(compute(str));  
             out.flush();  
             out.close();  
             buffer.close();  
         }  
   
         public static int compute(String str) {  
             char[] s = str.toCharArray();  
             int n = s.length;  
             int[][]dp = new int[n][n];  
             for (int i = 0; i < n; i++) {  
                 for (int j = 0; j < n; j++) {  
                     dp[i][j] = -1;  
                 }  
             }  
             return f1(s, 0, n - 1, dp);  
         }  
   
         //dp[i][j]:表示在s[i,j]的范围内区号区间匹配需要插入的括号数有多少  
         public static int f1(char[]s, int i, int j, int[][]dp) {  
             if (i == j) {  
                 return 1;  
             }  
             if (i + 1 == j) {  
                 if ((s[i] == '(' && s[j] == ')') || (s[i] == '[' && s[j] == ']')) {  
                     return 0;  
                 }  
                 return 2;  
             }  
             if(dp[i][j]!=-1){  
                 return dp[i][j];  
             }  
             //当i,j之间有多个字符时  
             //这个时候有两种可能  
             //第一种可能:i,j是一组符号,即 [l]、[r]本来就是配对的  
             //第二种可能:i,j之间可以并行,即有2组符号,即基于每个可能的划分点,做左右划分  
             int p1=Integer.MAX_VALUE;  
             if ((s[i] == '(' && s[j] == ')') || (s[i] == '[' && s[j] == ']')){  
                 p1=f1(s,i+1,j-1,dp);  
             }  
             int p2=Integer.MAX_VALUE;  
             for(int m=i;m<j;m++){  
                 p2=Math.min(p2,f1(s,i,m,dp)+f1(s,m+1,j,dp));  
             }  
             int ans=Math.min(p1,p2);  
             dp[i][j]=ans;  
             return ans;  
         }  
     }  
 }

2、涂色 & 奇怪打印机

测试链接:

给你一个字符串 s ,你的任务是计算这个打印机打印它需要的最少打印次数。 

示例 1:

输入:s = "aaabbb" 输出:2 解释:首先打印 "aaa" 然后打印 "bbb"。

示例 2:

输入:s = "aba" 输出:2 解释:首先打印 "aaa" 然后在第二个位置打印 "b" 覆盖掉原来的字符 'a'。

提示:

  • 1 <= s.length <= 100

  • s 由小写英文字母组成

解题思路:

  • 定义 dp[l][r] 为子串 s[l..r] 刷成目标串的最少次数。

  • 首先明确一点先从两端刷起,一定不会错过最优解

  • s[l]==s[r],那么l,r位置可以同时刷掉,即将[l,r]一股脑刷完

    • 按照以往的思路我们接下来应该考虑的是[l+1,r-1]范围上的内容

    • 但是这里我们考虑的是[l,r-1]的内容,r位置将在[l,r-1]最后刷的时候顺带刷掉

    • 为什么是[l,r-1]呢?这是因为我们不知道[l+1,r-1]之间是否还有其它位置的颜色与l,r相同

    • 有可能在第一次刷l,r的时候,内部的也被解决了,如AAAAAA这种

  • 若不等

    • 则基于范围上划分点的可能性展开

项目

内容

题目编号 / 标题

P4170 [CQOI2007] 涂色 / LeetCode 664 Strange Printer

题目关键词

涂色、区间 DP、字符串、最少操作次数、覆盖、区间划分

抽象问题类型

区间动态规划(Interval DP)典型的「括号化 / 区间分割」类问题

数据规模

n ≤ 50(洛谷),n ≤ 100(LeetCode 664)O(n³) 算法可过

状态设计

dp[l][r] 表示将子串 s[l..r] 刷成目标的最少次数

转移方程

- 若 s[l] == s[r]: dp[l][r] = dp[l][r-1] (最后一次涂 r 时,可以顺带覆盖 l)- 否则: dp[l][r] = min_{m=l..r-1} ( dp[l][m] + dp[m+1][r] )

边界条件

- dp[i][i] = 1(单个字符刷 1 次)- l > r 时为 0

解法要点

- 这是典型的「最后一步」区间 DP:考虑最后一次涂色操作覆盖哪些位置。- s[l]==s[r] 时可合并减少一次操作,是本题的关键优化。

复杂度

状态数 O(n²),转移 O(n),总时间 O(n³),空间 O(n²)

常见坑点

- 忘记处理 s[l]==s[r] 的优化,导致多刷 1 次- 枚举分割点时区间范围写错- dp 初始化不当(需区分 -1 / 0)

归类标签

- 区间 DP- 最少操作次数- 字符串覆盖问题

触发词

“连续区间操作覆盖”“最后一次操作顺带覆盖端点”“区间内求最优” ⇒ 考虑 区间 DP

一句话总结

通过定义 dp[l][r],枚举最后一次涂色位置,利用首尾相同可合并减少次数,把问题转化为典型的区间 DP。

代码如下:

 package main.java.class077;  
   
 import java.io.*;  
   
 /**  
  * @program: ZuoChengxunAlgorithmClass  
  * @ClassName Code02_Coloring  
  * @description: 涂色 & 奇怪打印机  
  * // 假设你有一条长度为5的木板,初始时没有涂过任何颜色  
  * // 你希望把它的5个单位长度分别涂上红、绿、蓝、绿、红  
  * // 用一个长度为5的字符串表示这个目标:RGBGR  
  * // 每次你可以把一段连续的木板涂成一个给定的颜色,后涂的颜色覆盖先涂的颜色  
  * // 例如第一次把木板涂成RRRRR  
  * // 第二次涂成RGGGR  
  * // 第三次涂成RGBGR,达到目标  
  * // 返回尽量少的涂色次数  
  * // 测试链接 : https://www.luogu.com.cn/problem/P4170  
  * // 测试链接 : https://leetcode.cn/problems/strange-printer/  
  * @author: zs宝  
  * @create: 2025-09-16 09:27  
  * @Version 1.0  
  **/public class Code02_Coloring {  
     public static void main(String[] args) throws IOException {  
         BufferedReader buffer=new BufferedReader(new InputStreamReader(System.in));  
         PrintWriter out=new PrintWriter(new OutputStreamWriter(System.out));  
         String str=buffer.readLine();  
         out.println(strangePrinter1(str));  
         out.flush();  
         out.close();  
         buffer.close();  
     }  
   
     //记忆化搜索的版本  
     public static int strangePrinter1(String str){  
         char[]s=str.toCharArray();  
         int n=s.length;  
         int[][]dp=new int[n][n];  
         for(int i=0;i<n;i++){  
             for(int j=0;j<n;j++){  
                 dp[i][j]=-1;  
             }  
         }  
         return f1(s,0,n-1,dp);  
     }  
   
     public static  int f1(char[]s,int l,int r,int[][]dp){  
         if(l>r){  
             return 0;  
         }  
         if(l==r){  
             return 1;  
         }  
         if(l+1==r){  
             return s[l]==s[r]?1:2;  
         }  
         if(dp[l][r]!=-1){  
             return dp[l][r];  
         }  
         int ans=Integer.MAX_VALUE;  
         //当l,r之间有多个的时候  
         //先从两端刷起,一定不会错过最优解  
         //这是因为:两端刷完后,范围变小只用刷中间的,两段不会再改变  
         if(s[l]==s[r]){  
             //若s[l]==s[r],那么l,r位置可以同时刷掉,即将[l,r]一股脑刷完  
             //按照以往的思路我们接下来应该考虑的是[l+1,r-1]范围上的内容  
             //但是这里我们考虑的是[l,r-1]的内容,r位置将在[l,r-1]刷的时候顺带刷掉  
             //为什么是[l,r-1]呢?这是因为我们不知道[l+1,r-1]之间是否还有其它位置的颜色与l,r相同  
             //有可能在第一次刷l,r的时候,内部的也被解决了,如AAAAAA这种  
             ans=Math.min(ans,f1(s,l,r-1,dp));  
         }else {  
             //若不等,则基于范围上划分点的可能性展开  
             for(int m=l;m<r;m++){  
                 ans=Math.min(ans,f1(s,l,m,dp)+f1(s,m+1,r,dp));  
             }  
         }  
         dp[l][r]=ans;  
         return ans;  
     }  
   
     //严格依赖位置的动态规划  
     public static int strangePrinter2(String str){  
         char[]s=str.toCharArray();  
         int n=s.length;  
         int[][]dp=new int[n][n];  
         dp[n-1][n-1]=1;  
         for(int i=0;i<n-1;i++){  
             dp[i][i]=1;  
             dp[i][i+1]=s[i]==s[i+1]?1:2;  
         }  
         for(int l=n-3;l>=0;l--){  
             for(int r=l+2;r<n;r++){  
                 dp[l][r]=Integer.MAX_VALUE;  
                 if(s[l]==s[r]){  
                     dp[l][r]=Math.min(dp[l][r],dp[l][r-1]);  
                 }else{  
                     for(int m=l;m<r;m++){  
                         dp[l][r]=Math.min(dp[l][r],dp[l][m]+dp[m+1][r]);  
                     }  
                 }  
             }  
         }  
         return dp[0][n-1];  
     }  
 }

3、合唱队

题目描述: 为了在即将到来的晚会上有更好的演出效果,作为 AAA 合唱队负责人的小 A 需要将合唱队的人根据他们的身高排出一个队形。假定合唱队一共 n 个人,第 i 个人的身高为 hi​ 米(1000≤hi​≤2000),并已知任何两个人的身高都不同。假定最终排出的队形是 A 个人站成一排,为了简化问题,小 A 想出了如下排队的方式:他让所有的人先按任意顺序站成一个初始队形,然后从左到右按以下原则依次将每个人插入最终排出的队形中:

  • 第一个人直接插入空的当前队形中。

  • 对从第二个人开始的每个人,如果他比前面那个人高(h 较大),那么将他插入当前队形的最右边。如果他比前面那个人矮(h 较小),那么将他插入当前队形的最左边。

当 n 个人全部插入当前队形后便获得最终排出的队形。

例如,有 6 个人站成一个初始队形,身高依次为 1850,1900,1700,1650,1800,1750, 那么小 A 会按以下步骤获得最终排出的队形:

  • 1850。

  • 1850,1900,因为 1900>1850。

  • 1700,1850,1900,因为 1700<1900。

  • 1650,1700,1850,1900,因为 1650<1700。

  • 1650,1700,1850,1900,1800,因为 1800>1650。

  • 1750,1650,1700,1850,1900,1800,因为 1750<1800。

因此,最终排出的队形是 1750,1650,1700,1850,1900,1800。

小 A 心中有一个理想队形,他想知道多少种初始队形可以获得理想的队形。

请求出答案对 19650827 取模的值。 输入格式: 第一行一个整数 n。 第二行 n 个整数,表示小 A 心中的理想队形。 输出格式: 输出一行一个整数,表示答案 mod19650827 的值。 输入输出样例 输入

 4
 1701 1702 1703 1704

输出

 8

解题思路:

  • 定义三维 DP:

    • dp[l][r][0]:从子序列 arr[l..r] 形成目标队形的方案数,且 arr[l] 是最后一个被插入的

    • dp[l][r][1]:同理,arr[r] 是最后一个被插入的

    • 最终答案是 dp[1][n][0]+dp[1][n][1](mod19650827)

  • 每个范围枚举最后插入的是左端点,还是又断点的情况

惯性思维:

  • 这道题最开始惯性思维是思考谁最先插入的问题,但是从最先插入思考反而十分困难,各种判定模糊不清晰

代码如下:

 package main.java.class077;  
   
 import java.io.*;  
 import java.util.Arrays;  
   
 /**  
  * @program: ZuoChengxunAlgorithmClass  
  * @ClassName Code03_HeightAndChoir  
  * @description: 合唱队  
  * // 具体描述情打开链接查看  
  * // 测试链接 : https://www.luogu.com.cn/problem/P3205  
  * @author: zs宝  
  * @create: 2025-09-16 10:30  
  * @Version 1.0  
  **/public class Code03_HeightAndChoir {  
   
     public static int MAXN=1001;  
     public static int[]arr=new int[MAXN+1];  
     public static int n;  
     public static int[][] dp = new int[MAXN][2];  
     public static int MOD=19650827;  
   
     public static void main(String[] args) throws IOException {  
         BufferedReader buffer=new BufferedReader(new InputStreamReader(System.in));  
         StreamTokenizer in=new StreamTokenizer(buffer);  
         PrintWriter out=new PrintWriter(new OutputStreamWriter(System.out));  
         while (in.nextToken()!=StreamTokenizer.TT_EOF){  
             n=(int)in.nval;  
             for(int i=1;i<=n;i++){  
                 in.nextToken();  
                 arr[i]=(int)in.nval;  
             }  
             if(n==1){  
                 out.println(1);  
             }else{  
                 out.println(compute2());  
             }  
         }  
         out.flush();  
         out.close();  
         buffer.close();  
     }  
   
     public static int compute1(){  
         //dp[i][j]:表示arr[i,j]的方式有多少种  
         // dp[l][r][0] : 形成l...r的状况的方法数,同时要求l位置的数字是最后出现的  
         // dp[l][r][1] : 形成l...r的状况的方法数,同时要求r位置的数字是最后出现的  
         int[][][]dp=new int[n+1][n+1][2];  
         for(int i=1;i<=n-1;i++){  
             if(arr[i]<arr[i+1]){  
                 dp[i][i+1][0]=1;  
                 dp[i][i+1][1]=1;  
             }  
         }  
         for(int i=n-2;i>=1;i--){  
             for(int j=i+2;j<=n;j++){  
                 //若最后一个插入的是左边界的点i  
                 if(arr[i]<arr[i+1]){  
                     dp[i][j][0]=(dp[i][j][0]+dp[i+1][j][0])%MOD;  
                 }  
                 if(arr[i]<arr[j]){  
                     dp[i][j][0]=(dp[i][j][0]+dp[i+1][j][1])%MOD;  
                 }  
                 //若最后插入的是右边界的点j  
                 if(arr[j]>arr[j-1]){  
                     dp[i][j][1]=(dp[i][j][1]+dp[i][j-1][1])%MOD;  
                 }  
                 if(arr[j]>arr[i]){  
                     dp[i][j][1]=(dp[i][j][1]+dp[i][j-1][0])%MOD;  
                 }  
             }  
         }  
         return (dp[1][n][0]+dp[1][n][1])%MOD;  
     }  
   
     //严格按照位置的动态规划+空间压缩  
     public static int compute2(){  
         for(int i=1;i<=n;i++){  
             Arrays.fill(dp[i],0);  
         }  
         if(arr[n-1]<arr[n]){  
             dp[n][0]=1;  
             dp[n][1]=1;  
         }  
         for(int i=n-2;i>=1;i--){  
             if(arr[i]<arr[i+1]){  
                 dp[i+1][0]=1;  
                 dp[i+1][1]=1;  
             }  
             for(int j=i+2;j<=n;j++){  
                 int a=0;  
                 int b=0;  
                 //若最后一个插入的是左边界的点i  
                 if(arr[i]<arr[i+1]){  
                     a=(a+dp[j][0])%MOD;  
                 }  
                 if(arr[i]<arr[j]){  
                     a=(a+dp[j][1])%MOD;  
                 }  
                 //若最后插入的是右边界的点j  
                 if(arr[j]>arr[j-1]){  
                     b=(b+dp[j-1][1])%MOD;  
                 }  
                 if(arr[j]>arr[i]){  
                     b=(b+dp[j-1][0])%MOD;  
                 }  
                 dp[j][0]=a;  
                 dp[j][1]=b;  
             }  
         }  
         return (dp[n][0]+dp[n][1])%MOD;  
     }  
 }

4、移除盒子

题目描述: 给出一些不同颜色的盒子 boxes ,盒子的颜色由不同的正数表示。

你将经过若干轮操作去去掉盒子,直到所有的盒子都去掉为止。每一轮你可以移除具有相同颜色的连续 k 个盒子(k >= 1),这样一轮之后你将得到 k * k 个积分。

返回 你能获得的最大积分和 。

示例 1:

输入:boxes = [1,3,2,2,2,3,4,3,1] 输出:23 解释: [1, 3, 2, 2, 2, 3, 4, 3, 1] ----> [1, 3, 3, 4, 3, 1] (33=9 分) ----> [1, 3, 3, 3, 1] (11=1 分) ----> [1, 1] (33=9 分) ----> [] (22=4 分)

示例 2:

输入:boxes = [1,1,1] 输出:9

示例 3:

输入:boxes = [1] 输出:1

提示:

  • 1 <= boxes.length <= 100

  • 1 <= boxes[i] <= 100

解题思路:

  • 这个题很难很难很难很难

  • 但是最后的思路就只有两种

    • 前缀先消,即能够拿到的某个数字连续前缀直接消掉,不考虑能否和后面相同的数字再连起来的情况

      • 如2 2 2 2 3 2 2 2,当到达3时,前面的4个2已经消掉,前面的4个2不考虑先消掉中间的3和后面的2结合

    • 前缀跟后,即前面某个数字的连续前缀保留,消除中间的部分,可以和后面的相同连续数字连接起来,然后再消

      • 如2 2 2 2 3 2 2 2,当到达3时,由于后面由是2了,因此先将这个3消掉,让前面的2和后面的2连接起来一起消除。

  • 如上,便是本题的转化思路,主要时跟随这个思路以往没有见到过

项目名称

内容填写(每道题一行填写)

📘 题目编号 / 标题

LeetCode 546 + 移除盒子

🧠 题目关键词(原文关键词)

相同颜色的K个盒子,得到K* k的积分

🧩 抽象问题类型(题目本质)

区间dp

🔍 数据规模 / 限制

- 1 <= boxes.length <= 100 - 1 <= boxes[i] <= 100

🧭 我的初步思路

没思路

✅ 正确解法类型

区间dp

❗ 没想到的原因

前缀跟随的思想

📦 归入的题型分类

区间dp

🧠 触发词(以后遇到就联想)

在一段范围上连续消除的积分->前缀跟随的区间dp

🧪 解法一句话总结

对于找到的一段相同连续数字,做前缀消除和前缀跟后的分类讨论

代码如下:

 package main.java.class077;  
   
 /**  
  * @program: ZuoChengxunAlgorithmClass  
  * @ClassName Code04_RemoveBoxes  
  * @description: 移除盒子  
  * // 给出一些不同颜色的盒子boxes,盒子的颜色由不同的正数表示  
  * // 你将经过若干轮操作去去掉盒子,直到所有的盒子都去掉为止  
  * // 每一轮你可以移除具有相同颜色的连续 k 个盒子(k >= 1)  
  * // 这样一轮之后你将得到 k * k 个积分  
  * // 返回你能获得的最大积分总和  
  * // 测试链接 : https://leetcode.cn/problems/remove-boxes/  
  * @author: zs宝  
  * @create: 2025-09-17 09:13  
  * @Version 1.0  
  **/public class Code04_RemoveBoxes {  
     class Solution {  
         public int removeBoxes(int[] boxes) {  
             int n=boxes.length;  
             int[][][]dp=new int[n][n][n];  
             return f1(boxes,0,n-1,0,dp);  
         }  
   
         /**  
          l:左边界  
          r:右边界  
          k:l前面有多少数字是和l位置的数相同的,这些数字的位置必须连着的,如2 22 2 2 2这种待在一起才行  
          */  
         public int f1(int[] boxes,int l,int r,int k,int[][][]dp){  
             if(l>r){  
                 return 0;  
             }  
             if(dp[l][r][k]!=0){  
                 return dp[l][r][k];  
             }  
             //可能性1:前缀先消  
             int s=l;  
             while(s+1<=r && boxes[s+1]==boxes[l]){  
                 s++;  
             }  
             //最后s的位置是最后一个连续着和l位置相同的数的位置  
             int cnt=s+1-l+k;  
             //前缀先消  
             int ans=cnt*cnt+f1(boxes,s+1,r,0,dp);  
             //可能性二:前缀跟后  
             for(int m=s+2;m<=r;m++){  
                 //找到后面数字和l位置数字相同的开头,只需要找到开头  
                 //如2 222 3 22,只需要找到3后面第一个位置的2  
                 if(boxes[m]==boxes[l] && boxes[m]!=boxes[m-1]){  
                     // boxes[l] == boxes[m]是必须条件  
                     // boxes[m - 1] != boxes[m]是剪枝条件,避免不必要的调用  
                     ans=Math.max(ans,f1(boxes,s+1,m-1,0,dp)+f1(boxes,m,r,cnt,dp));  
                 }  
             }  
             dp[l][r][k]=ans;  
             return ans;  
         }  
     }  
 }

5、合并石头的最低成本

题目描述: 有 n 堆石头排成一排,第 i 堆中有 stones[i] 块石头。

每次 移动 需要将 连续的 k 堆石头合并为一堆,而这次移动的成本为这 k 堆中石头的总数。

返回把所有石头合并成一堆的最低成本。如果无法合并成一堆,返回 -1 。

示例 1:

输入:stones = [3,2,4,1], K = 2 输出:20 解释: 从 [3, 2, 4, 1] 开始。 合并 [3, 2],成本为 5,剩下 [5, 4, 1]。 合并 [4, 1],成本为 5,剩下 [5, 5]。 合并 [5, 5],成本为 10,剩下 [10]。 总成本 20,这是可能的最小值。

示例 2:

输入:stones = [3,2,4,1], K = 3 输出:-1 解释:任何合并操作后,都会剩下 2 堆,我们无法再进行合并。所以这项任务是不可能完成的。.

示例 3:

输入:stones = [3,5,1,2,6], K = 3 输出:25 解释: 从 [3, 5, 1, 2, 6] 开始。 合并 [5, 1, 2],成本为 8,剩下 [3, 8, 6]。 合并 [3, 8, 6],成本为 17,剩下 [17]。 总成本 25,这是可能的最小值。

提示:

  • n == stones.length

  • 1 <= n <= 30

  • 1 <= stones[i] <= 100

  • 2 <= k <= 30

解题思路:

  • 这个题当,k一定,石头数量也一定时,那么整体能不能最终合成一块就定了

  • 在最终一定能合成一块的基础上,那么中间的范围能合成多少块其实无需我们再考虑

  • 只需要不断的枚举即可。

  • 如何枚举:

    • 让最前面的保证合成一块,那么后面的就一定要合成出k-1快,即一直合成到不能再合成为止

项目名称

内容填写(每道题一行填写)

📘 题目编号 / 标题

LeetCode 1000 + 合并石头的最低成本

🧠 题目关键词(原文关键词)

连续的石头,每连续k个合成一个,最终合为1个的最低成本

🧩 抽象问题类型(题目本质)

区间dp

🔍 数据规模 / 限制

- n == stones.length - 1 <= n <= 30 - 1 <= stones[i] <= 100 - 2 <= k <= 30

🧭 我的初步思路

没有思路

✅ 正确解法类型

区间dp

❗ 没想到的原因

就是分析不出来

📦 归入的题型分类

区间dp

🧠 触发词(以后遇到就联想)

连续k个石头合并->区间dp

🧪 解法一句话总结

石头在最终一定能合成一堆的情况下,不断地枚举可能性

代码如下

 package main.java.class077;  
   
 /**  
  * @program: ZuoChengxunAlgorithmClass  
  * @ClassName Code05_MinimumCostToMergeStones  
  * @description: 合并石头的最低成本  
  * // 有 n 堆石头排成一排,第 i 堆中有 stones[i] 块石头  
  * // 每次 移动 需要将 连续的 k 堆石头合并为一堆,而这次移动的成本为这 k 堆中石头的总数  
  * // 返回把所有石头合并成一堆的最低成本  
  * // 如果无法合并成一堆返回-1  
  * // 测试链接 : https://leetcode.cn/problems/minimum-cost-to-merge-stones/  
  * @author: zs宝  
  * @create: 2025-09-17 10:08  
  * @Version 1.0  
  **/public class Code05_MinimumCostToMergeStones {  
     class Solution {  
         // 时间复杂度O(n^3)  
         // 优化策略来自于观察  
         // l.....r最终会变成几份其实是注定的,根本就无法改变  
         // 那么也就知道,满足(n - 1) % (k - 1) == 0的情况下,  
         // 0....n-1最终一定是1份,也无法改变  
         // 如果l.....r最终一定是1份  
         // 那么要保证l.....m最终一定是1份,m+1...r最终一定是k-1份  
         // 如果l.....r最终一定是p份(p>1)  
         // 那么要保证l.....m最终一定是1份,那么m+1...r最终一定是p-1份  
         // 怎么保证的?枚举行为中,m += k-1很重要!  
         // m每次跳k-1!  
         // 如果l.....r最终一定是1份  
         // 就一定能保证l.....m最终一定是1份  
         // 也一定能保证m+1...r最终一定是k-1份  
         // 不要忘了,加上最后合并成1份的代价  
         // 如果l.....r最终一定是p份  
         // 就一定能保证l.....m最终一定是1份  
         // 也一定能保证m+1...r最终一定是p-1份  
         // 不用加上最后合并成1份的代价  
         public int mergeStones(int[] stones, int k) {  
             int n=stones.length;  
             //如果最终不能合成一份  
             if((n-1)%(k-1)!=0){  
                 return -1;  
             }  
             //前缀和,在最前面增加一个0  
             //那么[l,r]的前缀和就为presum[r+1]-presum[l]  
             int[]presum=new int[n+1];  
             for(int i=0,j=1,sum=0;i<n;i++,j++){  
                 sum+=stones[i];  
                 presum[j]=sum;  
             }  
             //既然最终一定能合成一份,那么在范围内,能和多少,就让它合多少,不再考虑其他的了  
             // dp[l][r] : l...r范围上的石头,合并到不能再合并(份数是确定的),最小代价是多少  
             int[][]dp=new int[n][n];  
             for(int l=n-2,ans;l>=0;l--){  
                 for(int r=l+1;r<n;r++){  
                     ans=Integer.MAX_VALUE;  
                     for(int m=l;m<r;m+=(k-1)){  
                         ans=Math.min(ans,dp[l][m]+dp[m+1][r]);  
                     }  
                     if((r-l)%(k-1)==0){  
                         // 最终一定能划分成一份,那么就再加合并代价  
                         ans+=presum[r+1]-presum[l];  
                     }  
                     dp[l][r]=ans;  
                 }  
             }  
             return dp[0][n-1];  
         }  
     }  
 }

6、统计不同回文子序列

题目描述: 给你一个字符串 s ,返回 s 中不同的非空回文子序列个数 。由于答案可能很大,请返回对 109 + 7 取余 的结果。

字符串的子序列可以经由字符串删除 0 个或多个字符获得。

如果一个序列与它反转后的序列一致,那么它是回文序列。

如果存在某个 i , 满足 ai != bi ,则两个序列 a1, a2, ... 和 b1, b2, ... 不同。

示例 1:

输入:s = 'bccb' 输出:6 解释:6 个不同的非空回文子字符序列分别为:'b', 'c', 'bb', 'cc', 'bcb', 'bccb'。 注意:'bcb' 虽然出现两次但仅计数一次。

示例 2:

输入:s = 'abcdabcdabcdabcdabcdabcdabcdabcddcbadcbadcbadcbadcbadcbadcbadcba' 输出:104860361 解释:共有 3104860382 个不同的非空回文子序列,104860361 是对 109 + 7 取余后的值。

提示:

  • 1 <= s.length <= 1000

  • s[i] 仅包含 'a''b''c' 或 'd'

解题思路如下图:

项目名称

内容填写(每道题一行填写)

📘 题目编号 / 标题

LeetCode 730 + 统计不同回文子序列

🧠 题目关键词(原文关键词)

回文子序列

🧩 抽象问题类型(题目本质)

回文,子序列模型

🔍 数据规模 / 限制

- 1 <= s.length <= 1000 - s[i] 仅包含 'a''b''c' 或 'd'

🧭 我的初步思路

区间dp

✅ 正确解法类型

区间dp

❗ 没想到的原因

分类讨论的结果计算有问题

📦 归入的题型分类

区间dp

🧠 触发词(以后遇到就联想)

统计回文子序列数量->区间dp

🧪 解法一句话总结

根据边界字符是否相等做讨论,其中做相等时还要讨论中间是否还有与边界字符相同的字符,数量为多少

代码如下:

 package main.java.class077;  
   
 import java.util.Arrays;  
   
 /**  
  * @program: ZuoChengxunAlgorithmClass  
  * @ClassName Code06_CountDifferentPalindromicSubsequences  
  * @description: 统计不同回文子序列  
  * // 给你一个字符串s,返回s中不同的非空回文子序列个数  
  * // 由于答案可能很大,答案对 1000000007 取模  
  * // 测试链接 : https://leetcode.cn/problems/count-different-palindromic-subsequences/  
  * @author: zs宝  
  * @create: 2025-09-17 11:10  
  * @Version 1.0  
  **/public class Code06_CountDifferentPalindromicSubsequences {  
     class Solution {  
         public static int MOD = 1000000007;  
   
         public static int countPalindromicSubsequences(String str) {  
             char[] s = str.toCharArray();  
             int n = s.length;  
             //last存储s[i]上一次出现的位置  
             int[]last=new int[256];  
             //先分别计算出每个位置的字符最进的左右位置出现相同字符的位置  
             Arrays.fill(last,-1);  
             int[]left=new int[n];  
             for(int i=0;i<n;i++){  
                 left[i]=last[s[i]];  
                 last[s[i]]=i;  
             }  
             int []right=new int[n];  
             Arrays.fill(right,n);  
             for(int i=n-1;i>=0;i--){  
                 right[i]=last[s[i]];  
                 last[s[i]]=i;  
             }  
             //接下来做区间dp  
             long[][]dp=new long[n][n];  
             for(int i=0;i<n;i++){  
                 dp[i][i]=1;  
             }  
             for(int i=n-2;i>=0;i--){  
                 for(int j=i+1;j<n;j++){  
                     //s[i]!=s[j]时  
                     if(s[i]!=s[j]){  
                         dp[i][j]=dp[i+1][j]+dp[i][j-1]-dp[i+1][j-1]+MOD;  
                     }else{  
                         //s[i]==s[j]时  
                         int r=left[j];  
                         int l=right[i];  
                         if(l>r){  
                             //当i,j中间没有s[i]这个字符时  
                             dp[i][j]=2*dp[i+1][j-1]+2;  
                         }else if(l==r){  
                             //当i,j中间有一个s[i]这个字符时  
                             dp[i][j]=2*dp[i+1][j-1]+1;  
                         }else{  
                             //当i,j中间有多个s[i]这个字符时  
                             dp[i][j]=2*dp[i+1][j-1]-dp[l+1][r-1]+MOD;  
                         }  
                     }  
                     dp[i][j]%=MOD;  
                 }  
             }  
             return (int)dp[0][n-1];  
         }  
   
   
     }  
 }

参考资料