最优子结构
重叠子问题
无后效性
题目链接:http://lx.lanqiao.cn/problem.page?gpid=T83
题目大意:
有一个箱子容量为 V(正整数,0 <= V <= 20000),同时有 n 个物品(0 < n <= 30),每个物品有一个体积(正整数)。
要求 n 个物品中,任取若干个装入箱内,使箱子的剩余空间为最小。
解题思路:
最经典的 $dp$ 问题,直接上手
题解:
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn = 20010;
//dp[j]:当体积为j时,最大填充为dp[j]
int dp[maxn], w[35];
int main(){
// freopen("input.txt", "r", stdin);
int v, n;
cin >> v >> n;
for(int i = 0;i < n;i++){
cin >> w[i];
}
//n个物品
for(int i = 0;i < n;i++){
//总体积为v
for(int j = v;j >= w[i];j--){
dp[j] = max(dp[j], dp[j - w[i]] + w[i]);
}
}
printf("%d\n", v - dp[v]);
return 0;
}
/*
24
6
8
3
12
7
9
7
0
*/
题目链接:http://lx.lanqiao.cn/problem.page?gpid=T93
题目大意:
在不超过 $N$ 元(可以等于 $N$ 元)的前提 下,使每件物品的价格与重要度的乘积的总和最大。
解题思路:
约定,$dp[i][j]$ 表示判定了 $i$ 件物品,剩余钱数为 $j$ 时,每件物品的价格与重要度的乘积的总和最大。
典型的 $0/1$ 背包问题
$dp$ 方程:
其中,$v$ 表示物品的价格,$p$ 表示该物品的重要度。
题解:
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn = 30005;
//dp[i][j]表示判定了i件物品,剩余钱数为j时,每件物品的价格与重要度的乘积的总和最大
int dp[30][maxn] = {0};
int main(){
int n, m;
cin >> n >> m;
//物品件数
for(int i = 1;i <= m;i++){
int v, p;
cin >> v >> p;
//总钱数
for(int j = 1;j <= n;j++){
if(j >= v){
dp[i][j] = max( dp[i - 1][j], dp[i - 1][j - v] + v * p);
}else{
dp[i][j] = dp[i - 1][j];
}
}
}
printf("%d\n", dp[m][n]);
return 0;
}
/*
1000 5
800 2
400 5
300 5
400 3
200 2
3900
*/
题目链接:http://lx.lanqiao.cn/problem.page?gpid=T84
题目大意:
将整数 n 分成 k 份,且每份不能为空,任意两份不能相同(不考虑顺序)。
例如:n = 7,k = 3,下面三种分法被认为是相同的。
1,1,5; 1,5,1; 5,1,1;
问有多少种不同的分法。
解题思路:
考虑四种情况:
题解:
#include <iostream>
#include <cstdio>
using namespace std;
int dfs(int n, int k){
if(n == k || k == 1){
return 1;
}
if(n < k){
return 0;
}
//至少存在一个1的情况:总数 - 1, 个数 - 1
//一定不存在1的情况: k个数均≥2,则每个数都 - 1,总数 - k * 1,个数不变
return dfs(n - 1, k - 1) + dfs(n - k, k);
}
int main(){
int n, k;
cin >> n >> k;
printf("%d\n", dfs(n, k));
return 0;
}
/*
7 3
4
*/
题目链接:http://lx.lanqiao.cn/problem.page?gpid=T44
题目大意:
如果一个序列满足下面的性质,我们就将它称为摆动序列:
- 序列中的所有数都是不大于k的正整数;
- 序列中至少有两个数;
- 序列中的数两两不相等;
- 如果第 i – 1 个数比第 i – 2 个数大,则第 i 个数比第 i – 2 个数小;如果第 i – 1 个数比第 i – 2 个数小,则第 i 个数比第 i – 2 个数大。
比如,当 k = 3 时,有下面几个这样的序列:
1 2 1 3 2 1 2 1 3 2 3 2 3 1 3 1 3 2
一共有 8 种,给定 k ,请求出满足上面要求的序列的个数。
解题思路:
题解:
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 30;
int arr[maxn], vis[maxn];
int ans = 0, n;
void dfs(int x){
if(x > 1){
if(x == 2){
ans++;
//公式满足条件3与条件4
}else if( (arr[x - 2] - arr[x - 3]) * (arr[x - 1] - arr[x - 3]) < 0){
ans++;
}else{
return;
}
}
for(int i = 1;i <= n;i++){
if(!vis[i]){
vis[i] = 1;
arr[x] = i;
dfs(x + 1);
vis[i] = 0;
}
}
}
int main(){
memset(vis, 0, sizeof(vis));
cin >> n;
dfs(0);
printf("%d", ans);
return 0;
}
/*
3
8
*/
题目链接:http://lx.lanqiao.cn/problem.page?gpid=T97
题目大意:
有 $n$ 个同学围成一圈,小蛮传了 $m$ 次球后,球又回到自己手中,问有多少种传球方法?
解题思路:
约定,$dp[i][j]$ 表示传了 $i$ 次球,到第 $j$ 个人手上的传球方法。
题解:
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn = 40;
//dp[i][j]表示传了i次球,到第j个人的次数
int dp[maxn][maxn] = {0};
int main(){
int n, m;
cin >> n >> m;
//传了0次球,到第一个人的次数,自然为1
dp[0][1] = 1;
for(int i = 1;i <= m;i++){
for(int j = 1;j <= n;j++){
int x = j - 1, y = j + 1;
//考虑第1个人的情况
if(j == 1){
x = n;
}
//考虑第n个人的情况
if(j == n){
y = 1;
}
//当前的情况等于左右传了i - 1次球的情况相加
dp[i][j] = dp[i - 1][x] + dp[i - 1][y];
}
}
printf("%d\n", dp[m][1]);
return 0;
}
/*
3 3
2
*/
题目链接:http://lx.lanqiao.cn/problem.page?gpid=T79
题目大意:
设有 N * N 的方格图(N <= 10),我们将其中的某些方格中填入正整数,而其他的方格中则放入数字 0。
某人从图的左上角的 A 点(1, 1)出发,可以向下行走,也可以向右走,直到到达右下角的 B 点(N, N)。在走过的路上,他可以取走方格中的数(取走后的方格中将变为数字 0 )。
此人从 A 点到 B 点共走两次,试找出 2 条这样的路径,使得取得的数之和为最大。
解题思路:
题解:
/*
模拟两个人同时从(1,1)走到(N,N),
转移方程:dp[x1][x2][y1][y2] = max(dp[x1 - 1][y1][x2 - 1][y2],
dp[x1][y1 - 1][x2][y2 - 1],
dp[x1 - 1][y1][x2][y2 - 1],
dp[x1][y1 - 1][x2 - 1][y2])
考虑两人走到同一点的特殊情况,只能加一次权值。
*/
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn = 20;
int g[maxn][maxn] = {0}, dp[maxn][maxn][maxn][maxn] = {0};
int main() {
// freopen("input.txt", "r", stdin);
int n;
cin >> n;
while(true){
int x, y, value;
cin >> x >> y >> value;
if(x == 0 && y == 0 && value == 0){
break;
}
g[x][y] = value;
}
for(int x1 = 1;x1 <= n;x1++){
for(int y1 = 1;y1 <= n;y1++){
for(int x2 = 1;x2 <= n;x2++){
for(int y2 = 1;y2 <= n;y2++){
int temp = max(dp[x1 - 1][y1][x2 - 1][y2], dp[x1][y1 - 1][x2][y2 - 1]);
temp = max(temp, dp[x1 - 1][y1][x2][y2 - 1]);
temp = max(temp, dp[x1][y1 - 1][x2 - 1][y2]);
if(x1 == x2 && y1 == y2){
dp[x1][y1][x2][y2] = temp + g[x1][y1];
}else{
dp[x1][y1][x2][y2] = temp + g[x1][y1] + g[x2][y2];
}
}
}
}
}
printf("%d\n", dp[n][n][n][n]);
return 0;
}
/*
8
2 3 13
2 6 6
3 5 7
4 4 14
5 2 21
5 6 4
6 3 15
7 2 14
0 0 0
67
*/
题目链接:http://lx.lanqiao.cn/problem.page?gpid=T13
题目大意:
$K$ 好数定义:一个自然数 $N$ 的 $K$进制表示中任意的相邻的两位都不是相邻的数字。求 $L$ 位 $K$ 进制数中 $K$ 好数的数目。输出它对 $1000000007$ 取模后的值。
解题思路:
我们约定,$dp[i][j]$ 表示长度为 $i$ ,以 $j$ 开头的 $K$ 好数个数。
$K$ 好数中除开头外,每位数的取值为:$0 \sim K-1$;
以 $K=3,L=3$ 为例:
以此类推长度为 $2$ ,长度为 $1$ 的情况;
因此可推出 $dp$ 方程:
另外要注意,$K$ 好数开头不能为 $0$ ;
题解:
#include <iostream>
#include <cstdio>
using namespace std;
#define MOD 1000000007
const int maxn = 105;
//dp[i][j]表示长度为i,以j开头的K好数个数
int dp[maxn][maxn];
int main(){
int k, l;
cin >> k >> l;
//dp[1][i]表示长度为1,以i开头的K好数个数,此时个数均为 1
for(int i = 0;i < k;i++){
dp[1][i] = 1;
}
//长度由2 → l
for(int i = 2;i <= l;i++){
//表示以j开头的个数,因为是K进制,所以K的范围为0 ~ K - 1
for(int j = 0;j < k;j++){
//枚举以j为开头时的所有情况
for(int x = 0;x < k;x++){
//符号K好数要求的数
if(x != j + 1 && x != j - 1){
//dp方程
dp[i][j] += dp[i - 1][x];
//注意取模
dp[i][j] %= MOD;
}
}
}
}
int ans = 0;
//开头不能为O,所以开头i的范围为1 ~ K - 1
for(int i = 1;i < k;i++){
ans += dp[l][i];
ans %= MOD;
}
printf("%d\n", ans);
return 0;
}
/*
4 2
7
*/