题目列表:
2020年:矩阵
2021年:砝码称重
3道经典线性DP问题(可能会考到类似的题哦):
摆花
合唱队形
导弹拦截
1道经典区间DP问题
石子合并
1.矩阵
答案:1430
分析:
这道题和“杨老师的照相排列”是同类型的题(“杨老师的照相排列”这道题属于经典题型,建议记住!!)
既然说到了,我们就来总结一下这一类的模板:
我们在排的时候的前提就是:前一排的数一定要大于后一排的数
s[0]~s[4]分别是第一排~第五排需要站的人数
f[a][b][c][d][e] 表示第一排a人,第二排b人,第三排c人,第四排d人,第五排e人时的方案数
f[0][0][0][0][0] = 1;
for (int a = 0; a <= s[0]; a ++ )
for (int b = 0; b <= s[1]; b ++ )
for (int c = 0; c <= s[2]; c ++ )
for (int d = 0; d <= s[3]; d ++ )
for (int e = 0; e <= s[4]; e ++ ){
LL &x = f[a][b][c][d][e];
if (a && a - 1 >= b){
x += f[a - 1][b][c][d][e];//第一排人数必须大于第二排
}
if (b && b - 1 >= c){
x += f[a][b - 1][c][d][e];//第二排人数必须大于第三排
}
if (c && c - 1 >= d){
x += f[a][b][c - 1][d][e];//第三排人数必须大于第四排
}
if (d && d - 1 >= e){
x += f[a][b][c][d - 1][e];//第四排人数必须大于第五排
}
if (e){
x += f[a][b][c][d][e - 1];
}
}
cout << dp[s[0]][s[1]][s[2]][s[3]][s[4]];
然后我们开始写这道题的代码把!
代码:
#include<iostream>
using namespace std;
int dp[1015][1015];
int main(){
dp[0][0] = 1;
for(int i = 0;i <= 1010;i++){
for(int j = 0;j <= 1010;j++){
if(i && i-1 >= j){
dp[i][j] += dp[i-1][j]%2020;
}
if(j){
dp[i][j] += dp[i][j-1]%2020;
}
}
}
cout << dp[1010][1010];
return 0;
}
2.砝码称重
分析:
这道题是01背包问题的变种
dp[i][j]:表示前i个砝码是否可以称出j的重量(dp[i][j]=1表示可以,dp[i][j]=0表示不可以)
我们再把i和j对应的范围确定一下:
1<=i<=N
-V<=j<=V(-V表示所有砝码放左边的重量和,V表示所有砝码放右边的重量和),但是又因为数组下标不能为负值,所有我们需要加上一个偏移量V
当前砝码有三种选择,不选,选放左边,选放右边
不放:
dp[i][j] |= dp[i-1][j](不放,所以上一个状态的重量应该还是j)
放左边(前提是放上后重量不小于-V):
dp[i][j] |= dp[i-1][j-w[i]](放左边重量增加,所以上一个状态的重量应该是j-w[i])
放右边(前提是放上后重量不超过V):
dp[i][j] |= dp[i-1][j+w[i]](放右边重量减少,所以上一个状态的重量应该是j+w[i])
三种状态只要有一种可以称出重量j,dp[i][j]就为true,所以状态转移之间是“或”的关系
好了,上代码:
代码:
#include<iostream>
using namespace std;
const int MAX_N = 110;
const int MAX_V = 1e5+10;
int N;
int w[MAX_N];
bool dp[MAX_N][2*MAX_V];
int main(){
cin >> N;
int V = 0;
for(int i = 1;i <= N;i++){
cin >> w[i];
V += w[i];
}
dp[0][V] = true;//什么都不放能称出重量为0,也是可以的
for(int i = 1;i <= N;i++){
for(int j = -V;j <= V;j++){
dp[i][j+V] |= dp[i-1][j+V];
if(j-w[i] >= -V){
dp[i][j+V] |= dp[i-1][j-w[i]+V];
}
if(j+w[i] <= V){
dp[i][j+V] |= dp[i-1][j+w[i]+V];
}
}
}
int ans = 0;
for(int i = 1;i <= V;i++){
if(dp[N][i+V]){
ans++;
}
}
cout << ans;
return 0;
}
3.摆花
分析:
这道题是完全背包问题的变种
dp[i][j]:表示前i种花共摆了j盆的方案数
属性:count
我们再把i和j对应的范围确定一下:
1<=i<=n
0<=j<=m
当前种花有不选,选放1盆,选放2盆,……,选放a[i]盆
不放:
dp[i][j] = dp[i-1][j]
放1盆:
dp[i][j] = dp[i-1][j-1]
放2盆:
dp[i][j] = dp[i][i-2]
……
放a[i]盆:
d[i][j] = dp[i-1][j-a[i]]
好了,上代码:
代码:
#include<iostream>
using namespace std;
int n,m;
const int N = 110,M = 110;
int a[N];
int dp[N][M];
int main(){
cin >> n >> m;
for(int i = 1;i <= n;i++){
cin >> a[i];
}
//初始化dp数组
for(int i = 0;i <= a[1];i++){
dp[1][i] = 1;
}
for(int i = 2;i <= n;i++){
for(int j = 0;j <= m;j++){
for(int k = 0;k <= a[i];k++){
if(j-k>=0){
dp[i][j] += dp[i-1][j-k]%1000007;
}
}
}
}
cout << dp[n][m];
return 0;
}
4.合唱队形
分析:
这道题是一道结合最长上升子序列和最长下降子序列的题
最少需要几位同学出列其实就是让我们算最多需要几位同学在队里
以第i位同学为中心,分别求出其左侧的最长上升子序列和其右侧的最长下降子序列
两者相加再减1就是合唱队的人数
代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int MAX_N = 110;
int N;
int h[MAX_N],l[MAX_N],r[MAX_N];
int main(){
cin >> N;
for(int i = 1;i <= N;i++){
cin >> h[i];
}
//求最长递增子序列
for(int i = 1;i <= N;i++){
l[i] = 1;
for(int j = 1;j < i;j++){
if(h[j] < h[i]){
l[i] = max(l[i],l[j]+1);
}
}
}
//求最长递减子序列
for(int i = N;i >= 1;i--){
r[i] = 1;
for(int j = N;j > i;j--){
if(h[j] < h[i]){
r[i] = max(r[i],r[j]+1);
}
}
}
//找最大组合
int k = 0;
for(int i = 1;i <= N;i++){
k = max(k,l[i]+r[i]-1);
}
cout << N-k;
return 0;
}
5.导弹拦截
分析:
求一套系统最多可以拦截多少导弹,就是求最长不上升子序列(注意是不高于,也就是后一枚导弹的高度<=前一枚导弹的高度)
求至少需要多少套系统,就是求最长递增子序列(为什么呢?我们如果仅需1套系统,那么就必须保证所有的数从左到右单调不递增,如果中间有数破坏了这个规则,就要增加一套系统,所以如果我们知道中间有多少个数破坏了这个单调不递增的规则,那么系统数不就知道了)
代码:
#include<iostream>
#include<sstream>
#include<algorithm>
using namespace std;
const int MAX_N = 110;
int h[MAX_N],d[MAX_N],u[MAX_N];
int main(){
string s;
getline(cin,s);
istringstream is(s);
int n = 0,k = 0;
while(is>>k){
h[++n] = k;
}
for(int i = 1;i <= n;i++){
d[i] = 1;
u[i] = 1;
for(int j = 1;j < i;j++){
if(h[j] >= h[i]){
d[i] = max(d[i],d[j]+1);
}
if(h[j] < h[i]){
u[i] = max(u[i],u[j]+1);
}
}
}
int md = 0,mu = 0;
for(int i = 1;i <= n;i++){
md = max(md,d[i]);
mu = max(mu,u[i]);
}
cout << md <<endl << mu;
return 0;
}
6.石子合并
分析:
dp[i][j]:表示将第i堆到第j堆合并成一堆的最小花费
状态转移方程:
dp[i][j] = dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]
区间dp问题的一般步骤:
1.枚举区间长度(2<=len<=n)
2.枚举左端点和右端点(1<=left<=n && j <= n)
3.枚举分界点的位置(i<=k<j,要能取到左端点)
代码:
#include<iostream>
#include<algorithm>
using namespace std;
const long long INF = 1e9;
const int N = 1010;
int n;
long long sum[N];
long long dp[N][N];
int main(){
cin >> n;
for(int i = 1;i <= n;i++){
cin >> sum[i];
sum[i] += sum[i-1];
}
dp[1][1] = 0;
for(int len = 2;len <= n;len++){
for(int i = 1;i+len-1 <= n;i++){
int j = i+len-1;
dp[i][j] = INF;
for(int k = i;k < j;k++){
dp[i][j] = min(dp[i][j],dp[i][k] + dp[k+1][j] + sum[j]-sum[i-1]);
}
}
}
cout << dp[1][n];
return 0;
}