题解:P12330 [蓝桥杯 2023 省 Java B] 合并石子

这篇题解是来给楼上大佬 MoonCake2011 的题解做补充的。很巧,我的思路和大佬的思路非常相似。这篇题解相对于大佬的题解或许讲的更加详细,对初学者更友好一些,所以管理大大求过

题目分析

很显然,做过弱化版的都知道,肯定就是用区间 DP 了。

什么是区间动态规划?

会的大佬请略过。

其实区间动态规划的含义很好理解,就是将普通的动态规划策略放到区间里而已,以此解决区间的一些最优解问题。解决的方式大体是相同的,都是通过一些子问题的最优解转移到大问题的最优解以此解决问题。放到区间动态规划里无非就是多个子区间对大区间的最优解的更新。

区间动态规划一般会有两个维度,分别表示一个区间的左端点和右端点。同时,我们使用循环来枚举区间,然后根据题意进行转移。

在必要的时候,我们还可以增加一些维度来表示更加复杂的状态。

接下来可以通过这道题的弱化版来给大家讲解最简单的区间动态规划。

题目讲解

我们可以发现,这道题目就是去掉了颜色的区分而已。我们可以设 \(f_{i,j}\) 为合并区间 \([i,j]\) 所需的最小花销。根据题意,我们就可以设立状态转移方程。

由于题目的意思是两个区间合并,那么我们可以枚举一个分割点 \(k\) 以此把目标区间分成两部分。枚举不同的 \(k\) 可以得到不同的合并花销。这些不同的方案中的最小值就是这个区间的最优合并花销了。

显然,公式为:

\[f_{i,j}=\min(f_{i,j},f_{l,k}+f_{k+1,j}+s_{i,j}) \]

代码实现

#include<bits/stdc++.h>
using namespace std;
int n,a[605],s[305][305],r,f[305][305]; 
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		a[n+i]=a[i];
	}
	for(int i=1;i<=n;i++)
	{
		s[i][i]=a[i];
		for(int j=i+1;j<=n;j++) s[i][j]=s[i][j-1]+a[j];//预处理区间数值和
	}
	for(int len=2;len<=n+1;len++)
	{
		for(int l=1;l+len-1<=n;l++)//枚举区间
		{
			r=l+len-1;
			f[l][r]=0x7f7f7f7f;
			for(int k=l;k<r;k++) f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+s[l][r]);//枚举分割点并且更新最优解
		}
	}
	printf("%d",f[1][n]);
	return 0;
}

本题题解

我们设 \({f_1}_{l,r,k}\) 为区间 \([l,r]\),颜色为 \(k\) 的合并最小开销,\(s_l\)\(s_r\) 分别为序列的前 \(l\) 个数的和以及序列的前 \(r\) 个数的和,\(pos\) 为枚举的分割点(\(l \le pos \le r-1\))。易得方程:

\[{f_1}_{l,r,0}=\min({f_1}_{l,r,0},{f_1}_{l,pos,2}+{f_1}_{pos+1,r,2}+s_r-s_{l-1}) \\ {f_1}_{l,r,1}=\min({f_1}_{l,r,1},{f_1}_{l,pos,0}+{f_1}_{pos+1,r,0}+s_r-s_{l-1}) \\ {f_1}_{l,r,2}=\min({f_1}_{l,r,2},{f_1}_{l,pos,1}+{f_1}_{pos+1,r,1}+s_r-s_{l-1}) \\ \]

接下来,我们再定义一个动态规划数组 \(f2\),这个数组用于答案的计算。

定义 \(f2_{i,j}\) 为将前 \(i\) 个石堆合并成 \(j\) 个石堆的最小花销,\(k\) 为枚举的分割点(\(0 \le k \le i-1\))。易得方程:

\[f2_{i,j}=\min(f2_{i,j},f2_{k,{j-1}}+\min(f1_{{k+1},i,0},\min(f1_{{k+1},i,1},f1_{{k+1},i,2}))) \]

代码实现

#include<bits/stdc++.h>
using namespace std;
int n,t[305],c[305],s[305],f1[305][305][5],f2[305][305],r;
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&t[i]);
        s[i]=s[i-1]+t[i];//计算前缀和
    }
    for(int i=1;i<=n;i++) scanf("%d",&c[i]);
    memset(f1,0x3f,sizeof f1);//初始化为极大值
    memset(f2,0x3f,sizeof f2);
    for(int i=1;i<=n;i++) f1[i][i][c[i]]=0;
    for(int len=2;len<=n;len++)
    {
        for(int l=1;l+len-1<=n;l++)
        {
            r=l+len-1;
            for(int k=l;k<r;k++)//参照上面的方程
            {
                f1[l][r][0]=min(f1[l][r][0],f1[l][k][2]+f1[k+1][r][2]+s[r]-s[l-1]);
                f1[l][r][1]=min(f1[l][r][1],f1[l][k][0]+f1[k+1][r][0]+s[r]-s[l-1]);
                f1[l][r][2]=min(f1[l][r][2],f1[l][k][1]+f1[k+1][r][1]+s[r]-s[l-1]);
            }
        }
    }
    f2[0][0]=0;//初始化
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=i;j++)
        {
            for(int k=0;k<i;k++)
            {
                int minn=min(f1[k+1][i][0],min(f1[k+1][i][1],f1[k+1][i][2]));
                f2[i][j]=min(f2[i][j],f2[k][j-1]+minn);//求出答案
            }
        }
    }
    for(int i=1;i<=n;i++)//枚举可能的答案
    {
        if(f2[n][i]<1e9)//假如这个答案符合标准,则这个答案一定是最优的
        {
            printf("%d %d",i,f2[n][i]);//输出答案
            return 0;
        }
    }
    return 0;
}

考虑到这个是 Java 的题目,但是蒟蒻根本不会 Java。于是我让 DeepSeek 帮我生成了一篇 Java 代码,供学习 Java 的同学参考。

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int[] t = new int[n + 1];
        int[] c = new int[n + 1];
        int[] s = new int[n + 1];
        
        // 读取石子数目并计算前缀和
        for (int i = 1; i <= n; i++) {
            t[i] = scanner.nextInt();
            s[i] = s[i - 1] + t[i];
        }
        
        // 读取石子颜色
        for (int i = 1; i <= n; i++) {
            c[i] = scanner.nextInt();
        }
        
        // 初始化三维DP数组 f1[l][r][k]
        int[][][] f1 = new int[n + 1][n + 1][3];
        // 初始化为极大值
        for (int i = 0; i <= n; i++) {
            for (int j = 0; j <= n; j++) {
                Arrays.fill(f1[i][j], Integer.MAX_VALUE / 2); // 防止溢出
            }
        }
        
        // 初始化二维DP数组 f2[i][j]
        int[][] f2 = new int[n + 1][n + 1];
        for (int i = 0; i <= n; i++) {
            Arrays.fill(f2[i], Integer.MAX_VALUE / 2);
        }
        
        // 初始化单堆石子
        for (int i = 1; i <= n; i++) {
            f1[i][i][c[i]] = 0;
        }
        
        // 区间DP计算 f1
        for (int len = 2; len <= n; len++) {
            for (int l = 1; l + len - 1 <= n; l++) {
                int r = l + len - 1;
                for (int k = l; k < r; k++) {
                    // 颜色转移规则:0+0→1, 1+1→2, 2+2→0
                    f1[l][r][0] = Math.min(f1[l][r][0], f1[l][k][2] + f1[k + 1][r][2] + s[r] - s[l - 1]);
                    f1[l][r][1] = Math.min(f1[l][r][1], f1[l][k][0] + f1[k + 1][r][0] + s[r] - s[l - 1]);
                    f1[l][r][2] = Math.min(f1[l][r][2], f1[l][k][1] + f1[k + 1][r][1] + s[r] - s[l - 1]);
                }
            }
        }
        
        // 初始化f2
        f2[0][0] = 0;
        
        // 计算f2:将前i堆合并成j堆的最小花费
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= i; j++) {
                for (int k = 0; k < i; k++) {
                    int minColor = Math.min(f1[k + 1][i][0], Math.min(f1[k + 1][i][1], f1[k + 1][i][2]));
                    f2[i][j] = Math.min(f2[i][j], f2[k][j - 1] + minColor);
                }
            }
        }
        
        // 查找最小堆数和对应的最小花费
        for (int i = 1; i <= n; i++) {
            if (f2[n][i] < 1e9) {
                System.out.println(i + " " + f2[n][i]);
                return;
            }
        }
    }
}
posted @ 2025-11-15 11:10  linruicong  阅读(2)  评论(0)    收藏  举报