/*
动态规划:状态的表示与计算
状态表示:
f(i,j)表示所有从起点走到(i,j)的路径中,数字和最大的路径
(某种集合的某种属性)
状态计算(集合划分):
f(i,j)可分为从左上角走到(i,j)或者从右上角走到(i,j)
f(i,j)=max( f(i-1,j-1)+a[i][j] , f(i-1,j)+a[i][j] )
*/
#include<iostream>
using namespace std;
const int N=510,INF=1e9;
int n;
int a[N][N];
int f[N][N];
int main(){
cin>>n;
for(int i=1;i<=n;i++){//读入数字三角形,下标从1开始
for(int j=1;j<=i;j++) cin>>a[i][j];
}
// 因为数据中可能有负数,为了防止从边界外转移过来(全局变量默认为0),
// 所以要将边界外赋值为-INF
for(int i=0;i<=n;i++){
//为了在计算状态时不处理边界,将所有状态初始化为负无穷
for(int j=0;j<=i+1;j++) f[i][j]=-INF;
}
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-1]+a[i][j],f[i-1][j]+a[i][j]);
}
int res=-INF;
for(int i=1;i<=n;i++) res=max(res,f[n][i]);
cout<<res<<endl;
return 0;
}
//倒序dp,不需要考虑边界问题
#include<iostream>
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];
}//全局变量默认初始化为0,所以最底层数据计算没有问题
}
cout<<f[1][1]<<endl;
}
AcWing 1015. 摘花生
/*
从集合角度考虑DP
状态表示:
某种集合的某种属性(属性一般为max,min,数量)
f(i,j)表示所有从(1,1)走到(i,j)的路线中摘到花生的最大值,最后所求恰好是f(n,m)
状态计算:
对应于集合划分,划分时注意不重不漏,不漏一定要满足,但是当所求属性为最值时划分集合是否重复不重要
很重要的划分依据:“最后”
本题根据最后一步(i,j)从哪儿来划分:从上面下来;从左边过来。两类取最大值
f(i,j) = max( f(i,j-1) , f(i-1,j) ) + w(i,j)
*/
#include<iostream>
#include<algorithm>
using namespace std;
const int N=110;
int n,m;
int w[N][N];
int f[N][N];
int main(){
int T;
cin>>T;
while(T--){
cin>>n>>m;
for(int i=1;i<=n;i++){//状态计算时需要用到上一行,上一列的信息,下标从1开始便于处理边界情况
for(int j=1;j<=m;j++){
cin>>w[i][j];
}
}
//计算状态,线性顺序
//注意是否需要初始化,这道题计算状态时,行列下标都从1开始,那对于那些下标为0的行列来说
//如果未特意初始化,全局变量默认为0,一定要多考虑边界行列状态计算时是否会有影响
//这道题中因为是求最值,并且都为非负整数,所以不初始化没有影响
//会说这么多是希望注意,很多题在这一步时是不初始化是会有影响的
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])+w[i][j];
}
}
cout<<f[n][m]<<endl;
}
return 0;
}
//空间压缩
#include<cstring>
#include<iostream>
using namespace std;
const int N=110;
int n,m;
int w[N][N];
int f[N];
int main(){
int T;
cin>>T;
while(T--){
cin>>n>>m;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>w[i][j];
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
f[j]=max(f[j],f[j-1])+w[i][j];
}
}
cout<<f[m]<<endl;
//空间压缩,由于多组数据,注意置0
memset(f, 0, sizeof f);
}
return 0;
}
AcWing 1018. 最低通行费
/*
动态规划的表示:一般网格图f(i,j),线形图f(i),背包问题第一维是物品,第二维是体积。需要经验
与上一题摘花生相似,但上道题要求走时只能往下或右走,这道题呢?
由于题目中的时间限制,等价于走时不能往回走,也就是只能往下或右走
所以两道题非常相似,只是一个求最大值,一个求最小值
也因此,两者对边界问题的处理不同
*/
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110, INF = 1e9;
int n;
int w[N][N];
int f[N][N];
int main()
{
cin>>n;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
cin>>w[i][j];
/*
这里与上一题不同,上一道题因为求的是最大值,且均为非负整数,所以下标为0的行列不初始化,
默认为0没有任何问题,但这里求的是最小值,均为非负整数,所以如果下标为0的行列不初始化,
全局变量默认为0,则状态计算时所有状态全部为0那么如何初始化呢?由于求的是最小值,
所以下标为0的行列全部初始化为正无穷即可
*/
for (int i = 0; i <= n; i ++ ) {
f[i][0] = f[0][i] = INF;
}
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
{
/*
这里边界问题中对于f(1,1)要特别注意,上道题求最大值时,上左均为0,取最大值加自身权重后没有
任何问题,这里对均为正无穷的上左求最小值加自身权重任为正无穷,所以要单独把f(1,1)拿出来特判
*/
if (i == 1 && j == 1) f[i][j] = w[i][j];
else {
f[i][j] = min(f[i - 1][j] + w[i][j], f[i][j - 1] + w[i][j]);
}
}
cout<<f[n][n];
return 0;
}
/*
空间压缩
*/
#include<iostream>
using namespace std;
const int N=110,INF=1e9;
int w[N][N],f[N];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++) cin>>w[i][j];
}
for(int i=0;i<=n;i++) f[i]=INF;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i==1&&j==1) f[j]=w[i][j];
else{
f[j]=w[i][j]+min(f[j],f[j-1]);
}
}
}
cout<<f[n]<<endl;
}
AcWing 1027. 方格取数
/*
状态表示:
上两道题都是走一次,这道题需要走两次,如何扩展到走两次呢?
走一次:f(i,j)表示所有从(1,1)走到(i,j)的路径的最大值
走两次:
可以两条路线同时走,可能会走过相同的格子,但对于这些格子的数只能计算一次
f(i1,j1,i2,j2)表示所有从(1,1)分别走到(i1,j1),(i2,j2)的路径的最大值
因为是同时走,所以第一条路线走k步到达(i1,j1),第二条路线同样走k步到达(i2,j2),因此
i1+j1=i2+j2=k
所以可以降维到三维
f(i1,j1,i2,j2)变为f(k,i1,i2),其中j1=k-i1,j2=k-i2
状态计算:
对于每条路线,各自都可以向下或向右走到(i1,j1),(i2,j2),所以两两组合,有四种情况可以划分集合
以及要特别注意,计算每个状态时,如果当前两条路线走到了相同的点,那么这个点的数只计算一次
*/
#include<iostream>
#include<algorithm>
using namespace std;
const int N=15;
int n;
int w[N][N];
int f[2*N][N][N];
int main(){
cin>>n;
int a,b,c;
while(cin>>a>>b>>c,a||b||c) w[a][b]=c;//输入到a,b,c均为0时停止,所以是||
for(int k=2;k<=n+n;k++){
for(int i1=1;i1<k;i1++){
for(int i2=1;i2<k;i2++){
int t=w[i1][k-i1];
if(i1!=i2) t+=w[i2][k-i2];//判断是否走到同一格子,因为同时走,必然同时到
int& x=f[k][i1][i2];
x=max(x,f[k-1][i1-1][i2-1]+t);
x=max(x,f[k-1][i1-1][i2]+t);
x=max(x,f[k-1][i1][i2-1]+t);
x=max(x,f[k-1][i1][i2]+t);
}
}
}
cout<<f[n+n][n][n]<<endl;
return 0;
}
AcWing 275. 传纸条
/*
这道题与方格取数问题只有一点不同,那就是在方格取数中,同一个格子可以重复走,只是格子的数只能被计算一次,而这道题是不能走相同的格子
但是这道题完全能够用方格取数的代码解决是因为不管在那道题中,最优解永远不会由两条相交的路径组成,两条路径相交时,一定能绕路找到更优的解
*/
#include <iostream>
using namespace std;
const int N = 55;
int n, m;
int w[N][N];
int f[N * 2][N][N];
int main()
{
cin>>n>>m;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
cin>>w[i][j];
for(int k=2;k<=n+m;k++){
for(int i1=1;i1<k;i1++){
for(int i2=1;i2<k;i2++){
int t=w[i1][k-i1];
if(i1!=i2) t+=w[i2][k-i2];
int& x=f[k][i1][i2];
x=max(x,f[k-1][i1-1][i2-1]+t);
x=max(x,f[k-1][i1-1][i2]+t);
x=max(x,f[k-1][i1][i2-1]+t);
x=max(x,f[k-1][i1][i2]+t);
}
}
}
cout<<f[n + m][n][n];//注意这里都是n因为是i1,i2的范围
return 0;
}
最长上升子序列模型
AcWing 895. 最长上升子序列
/*
DP
状态表示:
f[i]表示所有以第i个数结尾的上升子序列中长度最大的上升子序列的长度
满足某种条件的某种集合的某种属性
状态计算:
集合划分,这道题f[i]中第i个数一定存在于上升子序列中,
所以根据它的前一个数去分类,前一个数可能没有,可能是数组中第一个元素,
可能是数组中第二个元素,一直到可能是数组第i-1个元素
f[i]=max(f[j]+1) j=0,1,2...i-1;
f[0]到f[i-1]不一定都能取,因为可能有某个数大于f[i]
时间复杂度:O(n^2)
*/
#include<iostream>
using namespace std;
const int N=1010;
int n;
int a[N],f[N];
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
int res=0;
for(int i=1;i<=n;i++){
f[i]=1;//只要a[i]一个数
for(int j=1;j<i;j++){
if(a[j]<a[i]) f[i]=max(f[i],f[j]+1);
}
res=max(res,f[i]);
}
cout<<res<<endl;
return 0;
}
//记录转移过程
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int N=1010;
int n;
int a[N],f[N],g[N];//g[N]存储转移过程
vector<int> res;
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++){
f[i]=1;//只要a[i]一个数
g[i]=0;//表示第i个数没有从谁转移过来
for(int j=1;j<i;j++){
if(a[j]<a[i]) {
if(f[i]<f[j]+1){
f[i]=f[j]+1;//记录f[i]是从谁转移过来的
g[i]=j;
}
}
}
}
int k=0;//存储最大值的下标
for(int i=1;i<=n;i++){
if(f[k]<f[i]) k=i;
}
cout<<f[k]<<endl;
while(f[k]){
res.push_back(a[k]);
k=g[k];
}
for(int i=res.size()-1;i>=0;i--) cout<<res[i]<<' ';
return 0;
}
AcWing 896. 最长上升子序列 II
/*
与上道题一样,只是数据范围变大,上道题n^2做法会超时
考虑优化
依次遍历数组每个元素,将前面求得的最长上升子序列按长度分类
存储各个长度的上升子序列结尾的最小值,相同长度的上升子序列中,结尾更大的肯定
没有结尾较小的好,因为如果一个数可以接到较大的后面,也一定可以接到较小的后面
所以较大的没必要存下来,它是可被替换的,较小的适用范围更广
可证明各个长度的上升子序列结尾的最小值单调递增
当前遍历元素可以接到某一个上升子序列后面
换句话说,在不考虑边界情况的前提下,当前遍历元素可以替换这个递增的结尾序列中
第一个大于它的元素,也就是更新某个序列的结尾,
更新的意思是当前遍历的这个元素更适合当结尾
时间复杂度:对于每个元素二分查找 O(nlogn)
*/
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
int a[N];
int q[N];
int main()
{
cin>>n;
for (int i = 0; i < n; i ++ ) cin>>a[i];
//通用做法:两种二分查找都可以做,考虑边界情况
q[0]=a[0];
//len表示数组q中最后一个元素的下标,因为从0开始,所有输出为len+1
int len = 0;
for (int i = 1; i < n; i ++ ){
// 先插入第一个元素,从第二个元素开始遍历
// 在结尾递增序列中找第一个大于等于当前遍历的元素,边界情况是最大元素都比它小
if(q[len]<a[i]){//边界情况
q[++len]=a[i];
}
else{
int l = 0, r = len;
while (l < r)
{
int mid = (l + r) >> 1;
if (q[mid] >= a[i]) r= mid;
else l = mid + 1;
}
q[l] = a[i];
}
}
cout<<len+1<<endl;
return 0;
}
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
int a[N];
int q[N];
int main()
{
cin>>n;
for (int i = 0; i < n; i ++ ) cin>>a[i];
//通用做法:两种二分查找都可以做,考虑边界情况
q[0]=a[0];
//len表示数组q中最后一个元素的下标,因为从0开始,所有输出为len+1
int len = 0;
for (int i = 1; i < n; i ++ ){
// 先插入第一个元素,从第二个元素开始遍历
//在结尾递增序列中找最后一个小于当前遍历的元素,边界情况是最小元素都比它大
if(q[0]>=a[i]) q[0]=a[i];
else{
int l = 0, r = len;
while (l < r)
{
int mid = l + r +1>> 1;
if (q[mid] < a[i]) l = mid;
else r = mid - 1;
}
len=max(len,l+1);
q[l+1] = a[i];
}
}
cout<<len+1<<endl;
return 0;
}
#include <iostream>
using namespace std;
const int N = 100010;
int n,a[N],q[N];//存不同长度上升子序列的结尾最小值
int main()
{
cin>>n;
for (int i = 0; i < n; i ++ ) cin>>a[i];
int len = 0;//len表示数组q中有多少个不同长度的序列最小值
for (int i = 0; i < n; i ++ )
{//遍历,二分查找数组q中有多少个不同长度的序列最小值小于当前遍历元素的元素
int l = 0, r = len;
while (l < r)
{
int mid = (l + r + 1) >> 1;
if (q[mid] < a[i]) l = mid;
else r = mid - 1;
}
len = max(len, r + 1);
q[r + 1] = a[i];
}
cout<<len<<endl;
return 0;
}
AcWing 1017. 怪盗基德的滑翔翼
/*
确定滑行方向后就转化为了LIS问题,原问题相当于正向和反向以ai为结尾的最长上升子序列长度,
分别正向和反向各进行一次LIS,取得最大值即可。
*/
#include<iostream>
// #include<algorithm>
// #include<cstring>
using namespace std;
const int N=110;
int n;
int a[N],f[N];
int main(){
int T;
cin>>T;
while(T--){
cin>>n;
for(int i=0;i<n;i++) cin>>a[i];
int res=0;
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);
}
res=max(res,f[i]);
}
// memset(f,0,sizeof f);
for(int i=n-1;i>=0;i--){
f[i]=1;
for(int j=n-1;j>i;j--){
if(a[i]>a[j]) f[i]=max(f[i],f[j]+1);
}
res=max(res,f[i]);
}
cout<<res<<endl;
}
return 0;
}
#include<iostream>
#include<cstring>
using namespace std;
const int N=110;
int n;
int a[N],f[N];
int main(){
int T;
cin>>T;
while(T--){
cin>>n;
for(int i=0;i<n;i++) cin>>a[i];
int len=0;
for(int i=0;i<n;i++){
int l=0,r=len;
while(l<r){
int mid=(l+r+1)>>1;
if(f[mid]<a[i]) l=mid;
else r=mid-1;
}
f[l+1]=a[i];
len=max(len,l+1);
}
memset(f,0,sizeof f);
int len_2=0;
for(int i=n-1;i>=0;i--){
int l=0,r=len_2;
while(l<r){
int mid=(l+r+1)>>1;
if(f[mid]<a[i]) l=mid;
else r=mid-1;
}
f[l+1]=a[i];
len_2=max(len_2,l+1);
}
memset(f,0,sizeof f);
int res=(len_2>len)?len_2:len;
cout<<res<<endl;
}
return 0;
}
AcWing 1014. 登山
/*
求先严格上升再严格下降的最长子序列的长度,根据哪个点为峰值去分类
先正序求以每个点a[i]为结尾的最长上升子序列的长度,再逆序求以同一点a[i]为结尾的最长上升子序列的长度
两者相加减1取最大值(因为a[i]取了两次),上道题是两个序列整体求最大值
*/
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n;
int a[N];
int f[N], g[N];
int main()
{
cin>>n;
for (int i = 1; i <= n; i ++ ) cin>>a[i];
for (int i = 1; i <= n; i ++ )
{
f[i] = 1;
for (int j = 1; j < i; j ++ )
if (a[i] > a[j])
f[i] = max(f[i], f[j] + 1);
}
for (int i = n; i >= 1; i -- )
{
g[i] = 1;
for (int j = n; j > i; j -- )
if (a[i] > a[j])
g[i] = max(g[i], g[j] + 1);
}
int res = 0;
for (int i = 1; i <= n; i ++ ) res = max(res, f[i] + g[i] - 1);
cout<<res;
return 0;
}
#include<iostream>
#include<cstring>
using namespace std;
const int N=1010;
int n;
int a[N],f[N],q[N],p[N];
int main(){
cin>>n;
for(int i=0;i<n;i++) cin>>a[i];
int len=0;
for(int i=0;i<n;i++){
int l=0,r=len;
while(l<r){
int mid=(l+r+1)>>1;
if(f[mid]<a[i]) l=mid;
else r=mid-1;
}
f[l+1]=a[i];
len=max(len,l+1);
q[i]=l+1;
}
memset(f,0,sizeof f);
len=0;
for(int i=n-1;i>=0;i--){
int l=0,r=len;
while(l<r){
int mid=(l+r+1)>>1;
if(f[mid]<a[i]) l=mid;
else r=mid-1;
}
f[l+1]=a[i];
len=max(len,l+1);
p[i]=l+1;
}
int res=0;
for(int i=0;i<n;i++) res=max(res,q[i]+p[i]-1);
cout<<res<<endl;
return 0;
}
AcWing 482. 合唱队形
/*
上一道登山的题的对偶,最少去掉多少人,就是最多剩下多少人,也就是求先上升再下降的最长子序列的长度,改一下数据范围和输出就行
*/
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n;
int a[N];
int f[N], g[N];
int main()
{
cin>>n;
for (int i = 1; i <= n; i ++ ) cin>>a[i];
for (int i = 1; i <= n; i ++ )
{
f[i] = 1;
for (int j = 1; j < i; j ++ )
if (a[i] > a[j])
f[i] = max(f[i], f[j] + 1);
}
for (int i = n; i >&#