附注:
目前,题解还在更新当中,有什么疑惑,欢迎留言讨论
A.简小胡的背包
分析:典型的01背包,我们把这个简小胡的背包分成若干个集合,每一个集合里装有一定数量的物品和所装物品重量的限度(承载量),并且所装价值最大。我们在枚举每一个集合里的物品时,物品有选与不选的情况,如果不选当前物品i,那当前集合的最大价值就是前i-1个物品所装的物品最大价值;如果确定了选当前物品i,可以先求前i-1个物品的集合的最大价值再加上当前物品价值就是前i个物品的最大价值,当然别忘记还要与不选当前物品i的最大价值进行比较,有可能不选的最大价值还要高于选的情况
计算:f[i][j]:前i个物品,背包承载量为j的最大价值
f[i][j]=f[i-1][j]
f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i])
时间复杂度 o(N^2)
参考文献
C++ 代码
#include <iostream>
using namespace std;
const int N = 110, M = 1010;
int n, m;
int w[N], v[N];
int f[N][M];
int main()
{
//input
cin >> n >> m;
for (int i = 1; i <= n; ++ i) cin >> v[i] >> w[i];
//dp
for (int i = 1; i <= n; ++ i)
{
for (int j = 0; j <= m; ++ j)
{
f[i][j] = f[i - 1][j];//不选
if (j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);//选
}
}
//output
cout << f[n][m] << endl;
return 0;
}
优化版本
//优化到一维
#include<iostream>
#include<bits/stdc++.h>
using namespace std;
int n,m;
const int mx=505;
int w[mx],v[mx],dp[mx];
int main()
{
cin>>n>>m;
for(int i = 1; i <= n; i ++ )cin>>w[i]>>v[i];
for(int i = 1; i <= n; i ++ )
for(int j = m; j >= w[i]; j -- )
dp[j] = max(dp[j],dp[j-w[i]]+v[i]);
//这里要从后往前枚举,是为了避免污染状态,保证每个状态更新一次
cout<<dp[m]<<endl;
return 0;
}
//简单处理
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int f[MAXN]; //
int main()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++) {
int v, w;
cin >> v >> w;
for(int j = m; j >= v; j--)
f[j] = max(f[j], f[j - v] + w);
}
cout << f[m] << endl;
return 0;
}
B.采药和c.点菜问题和简小胡的背包类似
可以参考下上面,这里不写思路了,放个代码
//采药 其实代码都一样
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e6+10;
int n,m;
int f[N];
int main()
{
while(cin>>m>>n&&n&&m){
memset(f,0,sizeof f);
for(int i=0;i<n;i++)
{
int a,b;
cin>>a>>b;
for(int j=m;j>=a;j--)
f[j]=max(f[j],f[j-a]+b);
}
cout<<f[m]<<'\n';
}
return 0;
}
D.货币系统
分析:典型的完全背包问题,思路和之前类似,只不过,这里的物品可以无限次使用
首先你对第i个数字进行了你的抉择,但是因为完全背包优化的缘故,所以前一维还是 i,接着因为使用了 a[i] 的钱数,所以应该是 j−a[i]。
计算:f[i][j] 表示使用前i个物品,凑出值为m的方案数
,则
f[i][j] = f[i - 1][j] + f[i][j-v]
时间复杂度 o(N^2)
参考文献
C++ 代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 3010;
int n,m;
LL f[100][N];
LL v[N];
int main()
{
cin>>n>>m;
for (int i = 1; i <= n; i ++ )cin>>v[i];
for (int i = 0; i <= n; i ++ )f[i][0]=1;
for (int i = 1; i <= n; i ++ )
{
for (int j = 1; j <= m; j ++ )
{
f[i][j]+=f[i-1][j];
if(j>=v[i])f[i][j]+=f[i][j-v[i]];
}
}
cout << f[n][m] << '\n';
return 0;
}
//优化
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 3010;
int n,m;
LL f[N];
int main()
{
cin>>n>>m;
f[0]=1;
for (int i = 1; i <= n; i ++ )
{
int w;
cin>>w;
for (int j = w; j <= m; j ++ )
f[j]+=f[j-w];
}
cout << f[m] << '\n';
return 0;
}
忠哥的dp(II)和忠哥的dp(IV)不讲,忠哥的dp(III)不是dp问题,是约瑟环问题,这里也不讲
H.数字三角形
f(i,j)含义: 从最底层出发到第 i 行第 j 个数的最大路径和
每个点有两种选择: 向左上方走 和 向右上方走
对应的子问题为: f(i+1,j) 和 f(i+1,j+1)
倒序情况下不需要考虑边界条件
结果: f(1,1)
时间复杂度 o(N^2)
参考文献
C++ 代码
#include<bits/stdc++.h>
using namespace std;
const int N=510;
int f[N][N];
int n;
int main(){
cin>>n;
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
cin>>f[i][j];
}
}
for(int i=n;i>=1;i--){
for(int j=i;j>=1;j--){
f[i][j]=max(f[i+1][j],f[i+1][j+1])+f[i][j];
}
}
cout<<f[1][1]<<endl;
}
//当然不止我这一种解法,望大佬留言指点下
I.最长公共子序列
集合表示:f[i][j]表示a的前i个字母,和b的前j个字母的最长公共子序列长度
集合划分:以a[i],b[j]是否包含在子序列当中为依据,因此可以分成四类:
①a[i]不在,b[j]不在
max=f[i−1][j−1]
②a[i]a[i]不在,b[j]b[j]在
看似是max=f[i−1][j] , 实际上无法用f[i−1][j]表示,因为f[i−1][j]表示的是在a的前i-1个字母中出现,并且在b的前j个字母中出现,此时b[j]不一定出现,这与条件不完全相等,条件给定是a[i]一定不在子序列中,b[j]一定在子序列当中,但仍可以用f[i−1][j]]来表示,原因就在于条件给定的情况被包含在f[i−1][j]中,即条件的情况是f[i−1][j]的子集,而求的是max,所以对结果不影响。
例如:要求a,b,c的最大值可以这样求:max(max(a,b),max(b,c))虽然b被重复使用,但仍能求出max,求max只要保证不漏即可。
③a[i]在,b[j]不在 原理同②
④a[i]在,b[j]在 max=f[i−1][j−1]+1
实际上,在计算时,①包含在②和③的情况中,所以①不用考虑
时间复杂度 o(N^2)
参考文献
C++ 代码
#include <iostream>
using namespace std;
const int N = 1010;
int n , m;
char a[N] , b[N];
int f[N][N];
int main()
{
cin >> n >> m;
cin >> a + 1 >> b + 1;
for(int i = 1 ; i <= n ; i++)
for(int j = 1 ; j <= m ; j++)
{
f[i][j] = max(f[i - 1][j] , f[i][j - 1]);//②和③的情况一定存在,所以可以无条件优先判断
if(a[i] == b[j]) f[i][j] = max(f[i][j] , f[i - 1][j - 1] + 1);
}
cout << f[n][m] << endl;
return 0;
}
J.友好城市
我们需要注意一下导致交叉的前提是什么,肯定是两边的城市前后顺序不同
同时考虑两组前后顺序肯定是不行的所以我们固定一组
将某边城市固定并进行排序,再来看另一边,
我们可以发现,若另一边从1到n进行枚举时,若是升序则一定不会存在交叉(另一边也是升序)
所以只有降序的点会导致交叉,题目要求避免交叉的情况批准的最多
时间复杂度 o(N^2)
参考文献
C++ 代码
#include <iostream>
#include <cstring>
#include <algorithm>
#define endl '\n'
#define read(x) scanf_s("%d",&x)
#define mem(a,b) memset(a,b,sizeof(a))
#define fo(i,a,b) for(int i=a;i<=b;++i)
#define go(i,a,b) for(int i=a;i>=b;--i)
using namespace std;
typedef pair<int, int>PIT;
const int N = 5010;
int n;
PIT q[N];
int f[N];
int main()
{
scanf("%d", &n);
fo(i, 0, n - 1)scanf("%d%d", &q[i].first, &q[i].second);
sort(q, q + n);
int res = 0;
fo(i, 0, n - 1)
{
f[i] = 1;
fo(j, 0, i - 1)
{
if (q[i].second > q[j].second)
f[i] = max(f[i], f[j] + 1);
}
res = max(f[i] , res);
}
printf("%d\n", res);
return 0;
}
K.拦截导弹简单版
1 3 5 9我们可以发现该序列的最长上升子序列长度为4,因为他是单调上升的,
所以想覆盖该序列最少需要4个下降子序列,恰好和最长上升子序列相同。
因此我们可以发现求最少个下降子序列的个数==求最长上升子序列,
我们在第二问直接求一下该序列的最长上升子序列即可。
时间复杂度 o(N^2)
参考文献
C++ 代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N=1005;
int a[N],f[N],g[N]; //f[N]存下降子序列,g[N]存上升子序列
int main(){
int ans=0;
int n=0;
while(cin>>a[n],a[n]) n++;
for(int i=0;i<n;i++){
f[i]=1;
for(int j=0;j<i;j++){
if(a[i]<=a[j]) f[i]=max(f[i],f[j]+1);
}
ans=max(ans,f[i]);
}
printf("%d\n",ans);
int cnt=0;
//数据范围比较小,这一步写最长上升子序列I也可以过的
for(int i=0;i<n;i++){
int k=0;
while(k<cnt && g[k]<a[i]) k++;
g[k]=a[i];
if(k==cnt) cnt++;
}
printf("%d",cnt);
return 0;
}
L.装箱问题
题目大意:给你一些物品,让你把箱子装的尽量满,求剩余空隙。
既然要你剩余体积最小,显然体积就是价值。同时又减少了体积,所以体积又是体积,又是价值。
这又是一个01背包题。
完整代码,时间复杂度: O(nm)
#include <iostream>
using namespace std;
const int N = 40, M = 1e5+10;
int n, m;
int a[N];
int f[N][M * 2];
//f[i][j]表示考虑前i个物品,总体积不超过j的最大体积。
int main() {
cin >> m >> n;
for (int i = 1; i <= n; i ++ ) cin >> a[i];
for (int i = 1; i <= n; i ++ ) {
for (int j = 1; j <= m; j ++ ) {
f[i][j] = f[i - 1][j];
//不选当前物品
if (j >= a[i]) f[i][j] = max(f[i][j], f[i - 1][j - a[i]] + a[i]);
//选当前物品,但体积必须足够
}
}
cout << m - f[n][m] << endl; //注意是剩余体积
return 0;
}
M.玉米田
状态转移方程
dp[i][j]表示所有摆完前i行,且第i行状态为j的方案数
那么对于每一个合法的状态dp[i][b],上一行的状态为dp[i-1][a];
那么dp[i][b] += dp[i-1][a]枚举每一个上一行能转移到下一行的状态
如果我们能枚举出每个能从i-1行状态转移到第i行的状态,然后把所有能转移的状态全部加起来并取模就得到了所有能转移到第i行的合法状态
状态压缩dp一般都要求预先把合法的状态i能够转移到其他所有合法状态都预先处理出来
注意,这里有两个要求,第一是状态合法,第二是状态i能够转移到状态j,那么我们就把能转移的合法状态记录下来
即head[state[i]].push_back(state[i]);
#include <bits/stdc++.h>
using namespace std;
const int N=13,Mod=100000000;
vector<int> state,head[1<<N];
int n,m,x,f[14][1<<N],g[N];
inline bool check(int x)//快速判断有没有相邻的1
{
return !(x&x>>1);
}
inline void init()
{
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++)
for (int j=1; j<=m; j++)
{
scanf("%d",&x);
g[i]+=(!x<<(j-1));//荒废土地是0,我们在这里转换为1
}
for(int i=0; i<(1<<m); i++)
if (check(i))//这个状态不存在种植左右相邻的玉米
state.push_back(i);
for(int i=0; i<state.size(); i++)
for(int j=0; j<state.size(); j++)
if (!(state[i] & state[j]))//i对应的状态和j对应的状态没有在同一列种植玉米
head[i].push_back(j);
f[0][0]=1;
for(int i=1; i<=n+1; i++)
for(int a=0; a<state.size(); a++)
{
if (state[a] & g[i])//在第i行,状态a是否满足在荒废土地没有种植玉米
continue;
for(int b=0; b<head[a].size(); b++)//从上一行b对应的状态,转到本行a对应的状态
f[i][a]=(f[i][a]+f[i-1][head[a][b]])%Mod;
}
printf("%d\n",f[n+1][0]);//表示第n+1行什么都没种植的状态,其实就是累加f[n][S]
}
int main()
{
init();
return 0;
}
N.石子合并<2>
关键,看作一个环,实际是进行连边操作,看能够连多少边。
直接枚举最后一步的缺口在哪里,然后拉开转化为链式问题。
> 实际上就是枚举,但是这种枚举破开的方式,关键是怎么看待其实每个边的地位都一样这件事
> n^4
本质是什么?本质是求n个长度为n的链
> 将环形链复制在后面,复制一遍,实际就是对2n的链上做.
>2n^3
针对这个问题有两点需要解决
1:一个是如何一条链的石子合并,取得最大或者在最小值
这个我们可以使用区间DP来解决,枚举中间点来解决区间问题(遍历顺序需掌握)
2:这里是环形的,并非链状,如何解决呢?
对于这种环形问题我们可以将n扩大成两倍,来进行枚举n长度的问题
那为什么扩大成两倍就能解决环形问题呢?
首先环形问题我们无法知道最后的断口在哪所以如果暴力应该是枚举断口
但是当我们扩大后我们可以发现我们当初所枚举的断口所生成链都在我们现在这个2n的长度中
但是这也导致了一个我们每次必须枚举2n的长度但是一个8倍一个n倍显然可以接受
并且区间DP需要快速算出区间和所以这里用前缀和
我们这里取得最大值与最小值,所以不能简单地均初始化为0,因为可能导致最小值均为0
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 420,INF = 0x3f3f3f3f;
int n;
int s[N],w[N];
int f[N][N],g[N][N];
int main()
{
cin>>n;
for (int i = 1; i <= n; i ++ )
{
cin>>w[i];
w[n+i]=w[i];
}
memset(f,0x3f, sizeof f);
memset(g,0xcf, sizeof g);
for (int i = 1; i <= n*2; i ++ )s[i]=s[i-1]+w[i];
for(int len = 1; len <= n; len ++ )
{
for(int l = 1; l + len - 1 <= n*2; l ++ )
{
int r = l + len -1;
if(len==1)f[l][r]=g[l][r]=0;
else
{
for (int k = l; k < r; k ++ )
{
f[l][r] = min(f[l][r],f[l][k]+f[k+1][r]+s[r]-s[l-1]);
g[l][r] = max(g[l][r],g[l][k]+g[k+1][r]+s[r]-s[l-1]);
}
}
}
}
int minv = INF, maxv = -INF;
for (int i = 1; i <= n; i ++ )
{
minv = min(minv,f[i][i+n-1]);
maxv = max(maxv,g[i][i+n-1]);
}
cout << minv << '\n';
cout << maxv <<'\n';
return 0;
}
O.能量项链
其实就是矩阵乘法的变形
很神奇的是,矩阵乘法原来其实也可以从图形结合的角度看待,即图形的边与定点。
合并两个相关的点,其实就直接关联了三个边
(2,3) (3,5) (5,10) (10,2)
换种表达方式
2 3 5 10 2
f[l,r] 所有将[l,r]合并的方式的Max
这种划分方式,中间是公用的!
f[l,r] = max(f[l,r], f[l,k]+f[k,r]+w[l]w[k]w[r])
特殊情况解释:
1. 如何只有一个矩阵,代价为0,len从3开始枚举没问题
2. 最后一次合并相当于[l,k],[k,r]
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 210;
int n;
int w[N];
int f[N][N];
int main()
{
cin>>n;
for (int i = 1; i <= n; i ++ )
{
cin>>w[i];
w[n+i]=w[i];
}
for(int len = 3; len <= n + 1; len ++ )
for(int l = 1; len + l - 1 <= 2*n; l ++ )
{
int r = len + l - 1;
for (int k = l + 1; k < r; k ++ )
f[l][r] = max(f[l][r],f[l][k]+f[k][r]+w[l]*w[k]*w[r]);
}
int res = 0;
for (int i = 1; i <= n; i ++ )res = max(res,f[i][n+i]);
cout << res <<'\n';
return 0;
}
P.数的划分
首先这道题是一个跟组合数学有关的题目,我们把它转化成如下问题:
有n个小球放到k个盒子里,各个盒子无差别,每个盒子里必须要有小球,共有几种放法?
此时我们需要找状态转移方程,与之前的简单dp直接把大问题转化成小问题不同,本题要把大问题分成两部分,分别转化为小问题。这时我们需要讨论在此时的放法中是否存在某一个盒子只有一个小球的情况:
1.若存在,则该盒子和该小球并不影响放法的种数,于是dp[i][j]=dp[i-1][j-1]
2.若不存在,则每个盒子中都至少有两个小球,在这种情况下,我们从每个盒子中都拿走一个小球,也不影响放法的种数。于是dp[i][j]=dp[i-j][j]
结合起来就是大问题放法的种数,可得状态转移方程dp[i][j]=dp[i-1][j-1]+dp[i-j][j];
原文链接:https://blog.youkuaiyun.com/langzitan123/article/details/80164895
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int f[N][N];
int n,k;
int main()
{
cin>>n>>k;
f[0][0]=1;
for (int i = 1; i <= n; i ++ )
for(int j = 1; j<= i ; j ++ )
f[i][j] = f[i-1][j-1]+f[i-j][j];
cout << f[n][k] << '\n';
return 0;
}
不同路径这里不讲,放个代码
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int f[N][N];
int n,m;
int main()
{
cin>>n>>m;
f[1][1]=1;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
{
if(i-1==1)f[i-1][j]=1;
if(j-1==1)f[i][j-1]=1;
f[i][j]=f[i-1][j]+f[i][j-1];
}
return cout << f[n][m] ,0;
}
Z3.吃水果
我们设状态
dp[i][j]表示前i位有j个不同的方案数dp[i][j]表示前i位有j个不同的方案数
那么转移很好想到
dp[i][j]=dp[i−1][j]+dp[i−1][j−1]∗(m−1)
(前面已经凑出来jj个不同 dp[i−1][k]
(前面只凑出来j−1j−1个不同,因此当前的选择需要乘上(m−1)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 2010,mod = 998244353 ;
LL n,m,k;
LL f[N][N];
int main()
{
cin>>n>>m>>k;
f[1][0]=m;
for (LL i = 2; i <= n; i ++ )
{
f[i][0]=m;
for (LL j = 1; j <= k; j ++ )
f[i][j]=f[i-1][j]+(f[i-1][j-1]*(m-1))%mod;
}
cout << f[n][k]%mod <<'\n';
return 0;
}
附注:这题之前没想出来,确实气人hhh
好了,有问题欢迎留言(只要我看到了,会回复的)
嗷嗷嗷~