前言
在本次测试中,四个题目都是动态规划,可以说是动态规划专场了。
在第一题中,我们需要求解从起点到终点的最大和,通过定义状态和状态转移方程,我们可以逐步计算出最优解。
第二题是一个最优组合问题,我们需要找到组合出特定面值所需的最少张数。通过定义状态和状态转移方程,我们可以逐步求解出最优解。
第三题是一个区间问题,我们需要找到最少的切割次数。通过定义状态和状态转移方程,我们可以找到最优的切割方案。
最后一题是一个球的分配问题,我们需要计算将球分配到盒子中的方法数。通过定义状态和状态转移方程,我们可以计算出最终的结果。
最佳路径
如下所示的由正整数数字构成的三角形:
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[i−1][j]和左上方 , f [ i − 1 ] [ j − 1 ] ,f[i - 1][j - 1] ,f[i−1][j−1]两个点到达,假设 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[i−1][j],f[i−1][j−1]两个坐标的值,显然 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[i−1][j],f[i−1][j−1])+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<n≤100,0<k≤1000) 接下来的一行有 n n n个正整数 a i ( 0 < a i ≤ 1000 ) a_i(0 < a_i ≤ 1000) ai(0<ai≤1000)。 若 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[i−a[1]],f[i−a[2],f[i−a[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][r−1]==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
N,M≤5000
题目解析
动态规划,定义 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[i−1][j+1]加到 s s s 上。表示将一个球放入当前盒子,并将剩余的球分配到剩余的盒子中的方法数。如果当前盒子中球的数量大于上限 k k k,则通过减去回退 K K K步的盒子中分配球的方法数 f [ i − 1 ] [ j − k ] f[i - 1][j - k] f[i−1][j−k]更新 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;
}