2023年3月电子学会青少年软件编程C++四级题解

前言

在本次测试中,四个题目都是动态规划,可以说是动态规划专场了。

在第一题中,我们需要求解从起点到终点的最大和,通过定义状态和状态转移方程,我们可以逐步计算出最优解。

第二题是一个最优组合问题,我们需要找到组合出特定面值所需的最少张数。通过定义状态和状态转移方程,我们可以逐步求解出最优解。

第三题是一个区间问题,我们需要找到最少的切割次数。通过定义状态和状态转移方程,我们可以找到最优的切割方案。

最后一题是一个球的分配问题,我们需要计算将球分配到盒子中的方法数。通过定义状态和状态转移方程,我们可以计算出最终的结果。


最佳路径

如下所示的由正整数数字构成的三角形:

7
3 8
8 1 0
2 7 4 4
4 5 2 6 5

从三角形的顶部到底部有很多条不同的路径。对于每条路径,把路径上面的数加起来可以得到一个和,和最大的路径称为最佳路径。你的任务就是求出最佳路径上的数字之和。
注意:路径上的每一步只能从一个数走到下一层上和它最近的下边(正下方)的数或者右边(右下方)的数。

时间限制:1000

内存限制:65536

输入

第一行为三角形高度 100 > = h > = 1 100>=h>=1 100>=h>=1,同时也是最底层边的数字的数目。 从第二行开始,每行为三角形相应行的数字,中间用空格分隔。

输出

最佳路径的长度数值。

样例输入

5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5

样例输出

30
题目解析

这是一道经典的动态规划问题,对于每个点 f [ i ] [ j ] f[i][j] f[i][j],只能从正上方 f [ i − 1 ] [ j ] f[i - 1][j] f[i1][j]和左上方 , f [ i − 1 ] [ j − 1 ] ,f[i - 1][j - 1] ,f[i1][j1]两个点到达,假设 f [ i ] [ j ] f[i][j] f[i][j]为坐标 ( 1 , 1 ) (1,1) (1,1)到坐标 ( i , j ) (i,j) (i,j)的最大和,那么只要知道 f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − 1 ] f[i - 1][j], f[i - 1][j - 1] f[i1][j],f[i1][j1]两个坐标的值,显然 f [ i ] [ j ] f[i][j] f[i][j]就等于 m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − 1 ] ) + a [ i ] [ j ] max(f[i - 1][j], f[i - 1][j - 1]) + a[i][j] max(f[i1][j],f[i1][j1])+a[i][j],其中 a [ i ] [ j ] a[i][j] a[i][j]为坐标 ( i , j ) (i, j) (i,j)上的值。

代码
#include <bits/stdc++.h>

using namespace std;

int n, f[110][110], a[110][110]; 

int main() {
	
	// 处理输入 
	cin >> n;
	for(int i = 1; i <= n; i++) {
		for(int j = 1; j <= i; j++) {
			cin >> a[i][j];
		}
	}
	
	// 预处理第一行 
	f[1][1] = a[1][1];
	
	// 第一行已经预处理, 从第二行开始循环 
	for(int i = 2; i <= n; i++) {
		for(int j = 1; j <= i; j++) {
			f[i][j] = max(f[i - 1][j], f[i - 1][j - 1]) + a[i][j];
		} 
	}
	
	int res = 0;
	
	// 路径结尾在最后一行,找最后一行最大值即是答案
	for(int i = 1; i <= n; i++) {
		res = max(res, f[n][i]);
	} 
	
	// 输出答案 
	cout << res;
	 
	
    return 0;
}

邮票收集

小A是个邮票收集爱好家,他有n种面值的邮票,每种邮票都有无数张。一天小B想要寄信,需要一共面值和为k的邮票组合。小A想要知道拼出面值为k的邮票最少需要多少张。

时间限制:1000

内存限制:131072

输入

输入是多组数据。(不超过10组) 每组数据的第一行正整数n,k,表示邮票的种类数目和目标要拼出的钱。 ( 0 < n ≤ 100 , 0 < k ≤ 1000 ) (0 < n ≤ 100, 0 < k ≤ 1000 ) (0<n100,0<k1000) 接下来的一行有 n n n个正整数 a i ( 0 < a i ≤ 1000 ) a_i(0 < a_i ≤ 1000) ai(0<ai1000)。 若 n = k = 0 n=k=0 n=k=0表示输入结束。

输出

每组数据输出一行一个数,分别表示拼出 k k k需要的最少的邮票数量。 如果不存在能够拼出k的方案,输出 − 1 -1 1

样例输入

4 10
1 2 3 4
5 16
1 2 3 4 5
2 7
4 5
0 0

样例输出

3
4
-1

提示

第一组数据: $10 = 4+4+2 $

第二组数据: 16 = 5 + 5 + 5 + 1 16 = 5+5+5+1 16=5+5+5+1

第三组数据: 不存在

题目解析

还是动态规划,设 f [ i ] f[i] f[i]为组合出 i i i元所需最少张数,那么 f [ i ] = m i n ( f [ i − a [ 1 ] ] , f [ i − a [ 2 ] , f [ i − a [ 3 ] ] . . . ] ) + 1 f[i]=min(f[i - a[1]], f[i - a[2], f[i - a[3]]...]) + 1 f[i]=min(f[ia[1]],f[ia[2],f[ia[3]]...])+1,其中 a [ 1 ] , a [ 2 ] , a [ 3 ] . . . a[1],a[2],a[3]... a[1],a[2],a[3]...为所有不大于 i i i元面值的邮票。此题注意细节有 f f f数组刚开始要初始化为一个很大的整数。

代码
#include <bits/stdc++.h>
using namespace std;

int n, k;
int a[1010], f[1010];

int main() {
	
	// 不知道多少组输入,所以死循环
	while(1) {
		
		cin >> n >> k;
		
		// 都等于0表示输入结束,退出循环
		if(n == 0 and k == 0) {
			break;
		}
		
		// 初始化 f 数组所有数字为大整数。
		memset(f, 127, sizeof(f));
		
		// 预处理 0 元需要 0 张邮票
		f[0] = 0;
		
		for(int i = 1; i <= n; i++) {
			cin >> a[i];
		}
		// 预处理 a 数组从小到大排序
		sort(a + 1, a + n + 1);
		
		// 从 1 元开始递推到 k 元
		for(int i = 1; i <= k; i++) {
			// 循环查找 a[1], a[2], a[3]... 等金额不大于 i 元的邮票,找最小值
			for(int j = 1; j <= n; j++) {
				// 因为是从小到大排序的,如果邮票金额大于所需金额,直接退出循环
				if(a[j] > i) { 
					break;
				}
				// 递推 i 元最少需要多少张邮票
				f[i] = min(f[i], f[i - a[j]] + 1);
			}
		}
		
		// 如果 k 元还是那个最大值,说明已有金额不能组合出 k 元,设为-1
		if(f[k] >= (1 << 30)) {
			f[k] = -1;
		}
		
		cout << f[k] << "\n";
	}
	
	return 0;
}

切割回文

阿福最近对回文串产生了非常浓厚的兴趣。
如果一个字符串从左往右看和从右往左看完全相同的话,那么就认为这个串是一个回文串。例如,“abcaacba”是一个回文串,“abcaaba”则不是一个回文串。
阿福现在强迫症发作,看到什么字符串都想要把它变成回文的。阿福可以通过切割字符串,使得切割完之后得到的子串都是回文的。
现在阿福想知道他最少切割多少次就可以达到目的。例如,对于字符串“abaacca”,最少切割一次,就可以得到“aba”“acca”这两个回文子串。

时间限制:1000

内存限制:65536

输入

输入的第一行是一个整数$ T (T <= 20)$ ,表示一共有 T T T 组数据。 接下来的 T T T 行,每一行都包含了一个长度不超过的 1000 1000 1000 的字符串,且字符串只包含了小写字母。

输出

对于每组数据,输出一行。该行包含一个整数,表示阿福最少切割的次数,使得切割完得到的子串都是回文的。

样例输入

3
abaacca
abcd
abcba

样例输出

1
3
0

提示

对于第一组样例,阿福最少切割 1 1 1 次,将原串切割为“aba”“acca”两个回文子串。 对于第二组样例,阿福最少切割 3 3 3 次,将原串切割为“a”、“b”、“c”、“d”这四个回文子串。 对于第三组样例,阿福不需要切割,原串本身就是一个回文串。

题目解析

经典的区间dp,设 f [ l ] [ r ] f[l][r] f[l][r]为区间 [ l , r ] [l, r] [l,r]最少切割次数,那么当 l = = r l == r l==r(区间长度为1)或者 s [ l ] = = s [ r ] 并且 f [ l + 1 ] [ r − 1 ] = = 0 s[l] == s[r] 并且f[l + 1][r - 1]==0 s[l]==s[r]并且f[l+1][r1]==0时(区间 [ l , r ] [l, r] [l,r]为回文串), f [ l ] [ r ] = 0 f[l][r]=0 f[l][r]=0,否则我们枚举区间 [ l , r ] [l, r] [l,r]的分界点 k k k,取最小值即可。

代码
#include <bits/stdc++.h>
using namespace std;

string s;
int n, f[1010][1010];


void solve() {
	cin >> s;
	n = s.size();
	
	// 数组从 1 - n,在字符串前面添加一个特殊字符,使得有效下标从 1 开始、
	s = '#' + s;
	
	// 初始化 f 数组为最大值
	memset(f, 127, sizeof(f));
	
	// 长度为 1 的区间一定是回文串,预处理分割次数为 0
	for(int i = 1; i <= n; i++) {
		f[i][i] = 0;
	}
	
	// 从长度为 2 的区间开始模拟
	for(int i = 2; i <= n; i++) {
		
		// 枚举区间左端点
		for(int j = 1; j + i - 1 <= n; j++) {
			int l = j, r = j + i - 1;
			
			// 如果 s[l] == s[r] 并且 区间长度为2 或者区间 [l + 1, r - 1] 为回文串,则区间 [l, r] 为回文串。
			if(s[l] == s[r] and (i == 2 or f[l + 1][r - 1] == 0)) {
				f[l][r] = 0;
				continue;
			}
			
			// 否则循环枚举分界点,求最小值
			for(int k = l; k < r; k++) {
				// 注意 枚举分界点,说明分割了一次,所以要加一
				f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + 1);
			}
		}
	}
	
	cout << f[1][n];
}


int main() {
	
	int t;
	cin >> t;
	
	while(t--) {
		solve();
	}
	
	
	return 0;
}

小球放盒子

有N个相同的球,M个不同的盒子,每个盒子最多放K个球

请计算将这N个球全部放入盒子中的方案数模 1000007 1000007 1000007后的结果

时间限制:10000

内存限制:131072

输入

三个正整数,依次为N,M,K

输出

输出方案数模1000007后的结果

样例输入

4 2 3

样例输出

3

提示
总共有3种方案,依次为 { 3 , 1 },{ 2 , 2 },{ 1 , 3 }。 对于100%的数据, N , M ≤ 5000 N,M ≤ 5000 NM5000

题目解析

动态规划,定义 f [ i ] [ j ] f[i][j] f[i][j]为前 i i i个盒子中放 j j j个球的做法,对于每个盒子,将 f [ i ] [ 0 ] f[i][0] f[i][0] 初始化为 1 1 1,表示将任意数量的球分配到 0 0 0个盒子的方法数为 1 1 1。将当前盒子中球的数量 m m m 存储在变量 s s s 中。将分配球的方法数存储在 f [ i ] [ j ] f[i][j] f[i][j] 中更新 s s s,通过将前一个盒子中分配球的方法数 f [ i − 1 ] [ j + 1 ] f[i - 1][j + 1] f[i1][j+1]加到 s s s 上。表示将一个球放入当前盒子,并将剩余的球分配到剩余的盒子中的方法数。如果当前盒子中球的数量大于上限 k k k,则通过减去回退 K K K步的盒子中分配球的方法数 f [ i − 1 ] [ j − k ] f[i - 1][j - k] f[i1][jk]更新 s s s。表示将一个球放入当前盒子,并将剩余的球分配到剩余的盒子中的方法数,但跳过一个盒子(因为它已经包含K个球)。

代码
#include <bits/stdc++.h>
using namespace std;

int mod = 1000007;

int n, m, k, f[5010][5010];

int main() {
	
	cin >> n >> m >> k;
	
	// 预处理 表示将任意数量的球分配到 0 个盒子的方法数为 1 
	f[0][0] = 1;
	for (int i = 1; i <= m; i++) {
		f[i][0] = 1;
	}
	
	for (int i = 1; i <= m; i++) {
		int s = i;
		for (int j = 1; j <= n; j++) {
			// 将分配球的方法数存储在 f[i][j] 中更新 s
			f[i][j] = s;
			
			// 通过将前一个盒子中分配球的方法数 f[i - 1][j + 1] 加到 s 上
			s = (s + f[i - 1][j + 1]) % mod;
			
			// 如果当前盒子中球的数量大于上限 k
			// 则通过减去回退 k 步的盒子中分配球的方法数 f[i - 1][j - k] 更新 s
			if (j >= k) {
				s = (s - f[i - 1][j - k] + mod) % mod;
			}
		}
	}
	
	cout << f[m][n];
	
	return 0;
}
  • 37
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值