算法提高课 第一章 动态规划①(数字三角形、最长上升子序列模型)
提高课第一章 动态规划:数字三角形、最长上升子序列、状态机模型、状态压缩DP、区间DP、树形DP
1.1数字三角形模型
1018. 最低通行费
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 105;
int g[N][N],n;
int f[N][N];//f[i][j]:g[1][1]到g[i][j]中所有方案中花费最小的方案
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ )
{
for (int j = 1; j <= n; j ++ )
{
scanf("%d", &g[i][j]);
}
}
memset(f,0x3f3f3f3f,sizeof f);//由于求最小值,初始化一定为无穷大
for (int i = 1; i <= n; i ++ )
{
for (int j = 1; j <= n; j ++ )
{
if(i==1 && j==1) f[i][j] = g[i][j];//注意:左上角边界一定要特判
else f[i][j] = min(f[i-1][j],f[i][j-1]) + g[i][j];
}
}
cout<<f[n][n]<<endl;
return 0;
}
1027. 方格取数
本题不同于前几道模型,需要求出走两次取得的最大值
走两次:f[i1,j1,i2,j2]表示从(1,1),(1,1)分别走到(i1,j1),(i2,j2)的路径的最大值
如何处理“同一个格子不能被重复选择”?
只有在i1 + j1 == i2 + j2时,两条路径的格子才可能重合
f[k,i1,i2]表示所有从(1,1),(1,1)分别走到(i1,k-i1),(i2,k-i2)的路径的最大值
k表示两条路线当前走到的格子的横纵坐标之和 k = i1 + j1 = i2 + j2
四维DP
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 15;
int g[N][N],n;
int a,b,c;
int f[N][N][N][N];
int main()
{
cin>>n;
while(cin>>a>>b>>c,a||b||c)
{
g[a][b] = c;
}
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 t = g[x1][y1];
if(x1!=x2 && y1!=y2)
{
t += g[x2][y2];
}
int &a = f[x1][y1][x2][y2];
a = max(a,f[x1-1][y1][x2-1][y2] + t);
a = max(a,f[x1-1][y1][x2][y2-1] + t);
a = max(a,f[x1][y1-1][x2][y2-1] + t);
a = max(a,f[x1][y1-1][x2-1][y2] + t);
}
}
}
}
cout<<f[n][n][n][n]<<endl;
}
三维DP
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 15;
int w[N][N];
int f[2*N][N][N],n; //f[k][x1][x2]:从(1,1),(1,1)到(x1,y1)和(x2,y2)路径中最大数字和的路径,k为x1+y1=x2+y2
int a,b,c;
int main()
{
cin>>n;
while(cin>>a>>b>>c,a||b||c)
{
w[a][b] = c;
}
for (int k = 2; k <= 2*n; k ++ ) //枚举k,k相同时才有可能发生路径重合
{
for(int x1 = 1;x1<=n;x1++)//枚举x1,有k和x1就能推出y1
{
for(int x2 = 1;x2<=n;x2++)//枚举x2,有k和x2就能推出y2
{
int y1 = k - x1,y2 = k - x2;
if(y1<1||y1>n||y2<1||y2>n) continue;//y1、y2必须在合法范围内
int t = w[x1][y1];
if(y1!=y2) t += w[x2][y2]; //路径未重合时,相加;重合时,不加
int &x = f[k][x1][x2];//用x简化代码
x = max(x,f[k-1][x1-1][x2-1] + t);//两条路都从上边来
x = max(x,f[k-1][x1][x2] + t);//两条路都从左边来
x = max(x,f[k-1][x1-1][x2] + t);//第一条从上边,第二条从左边
x = max(x,f[k-1][x1][x2-1] + t);//第一条从左边,第二条从上边
}
}
}
cout<<f[2*n][n][n]<<endl;
return 0;
}
1.2 最长上升子序列模型(一)
裸题模板:895. 最长上升子序列
#include<iostream>
#include<cstring>
#include<stack>
using namespace std;
const int N = 1005;
int n,a[N],ans;
int f[N];//所有以i结尾的严格单调上升子序列的集合
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
f[i] = 1;//初始化,最短子序列为1
}
for(int i=2;i<=n;i++)//右端点
{
for(int j = 1;j<=i;j++)
{
if(a[j] < a[i]) f[i] = max(f[i],f[j] + 1);//如果a[i]左边有比它小的数,选取个数较多的集合
ans = max(ans,f[i]);//结果方案不一定包含最后一个数,因此需要取最大值
}
}
cout<<ans<<endl;
return 0;
}
1017. 怪盗基德的滑翔翼
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 105;
int h[N],f[N],n,t;
int ans = -1;
int main()
{
scanf("%d", &t);
while (t -- )
{
ans = -1;
scanf("%d", &n);
for (int i = 1; i <= n; i ++ )
{
scanf("%d", &h[i]);
}
for (int i = 1; i <= n; i ++ ) //正向求解LIS
{
f[i] = 1;
for (int j = 1; j < i; j ++ )
{
if(h[i] > h[j]) f[i] = max(f[i],f[j] + 1);
ans = max(ans,f[i]);
}
}
for(int i = n;i;i--) //反向求解LIS
{
f[i] = 1;
for(int j = n;j>i;j--) //注意循环顺序
{
if(h[i] > h[j]) f[i] = max(f[i],f[j] + 1);
ans = max(ans,f[i]);
}
}
cout<<ans<<endl;
}
return 0;
}
1014. 登山
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1005;
int a[N],n;
int fl[N]; //fl[i]:从左到右,以i为结尾的最长上升子序列的值
int fr[N];//fr[i]:从右到左,以i为结尾的最长上升子序列的值
int ans;
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ )
{
scanf("%d", &a[i]);
}
for (int i = 1; i <= n; i ++ ) //计算fl[i]
{
fl[i] = 1;
for (int j = 1; j < i; j ++ )
{
if(a[i] > a[j])
{
fl[i] = max(fl[i],fl[j] + 1);
}
}
}
for(int i = n;i;i--) //计算fr[i]
{
fr[i] = 1;
for(int j = n;j>i;j--)
{
if(a[i] > a[j])
{
fr[i] = max(fr[i],fr[j] + 1);
}
}
ans = max(ans,fl[i] + fr[i] - 1);//注意fl和fr计算i了两次,所以要减1
}
cout<<ans<<endl;
return 0;
}
1012. 友好城市
对其中一个岸的城市按编号递增排序,再求另一个岸的LIS
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 5005;
PII a[N];
int n,ans;
int f[N];
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ )
{
scanf("%d%d", &a[i].first, &a[i].second);
}
sort(a+1,a+n+1); //排序,保证一条岸的单调性
for (int i = 1; i <= n; i ++ ) //求另外一个岸的LIS,保证桥梁不相交
{
f[i] = 1;
for (int j = 1; j < i; j ++ )
{
if(a[i].second > a[j].second) f[i] = max(f[i],f[j] + 1);
}
ans = max(ans,f[i]);
}
cout<<ans<<endl;
return 0;
}
1016. 最大上升子序列和
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1005;
int a[N],n;
int f[N]; //f[i]:以a[i]为结尾的最长上升子序列和的最大值
int ans;
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ )
{
scanf("%d", &a[i]);
}
for (int i = 1; i <= n; i ++ )
{
f[i] = a[i];
for (int j = 1; j <= n; j ++ )
{
if(a[i] > a[j])
{
f[i] = max(f[i],f[j] + a[i]);
}
}
ans = max(ans,f[i]);
}
cout << ans << endl;
return 0;
}
1.2 最长上升子序列模型(二)
1010. 拦截导弹
无需使用优先队列,因为子序列结尾数组一定是升序有序的
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1005;
int n,a[N];
int f[N];
int g[N];//表示每个子序列结尾的数组
int ans,cnt;
int main()
{
while(cin>>a[n]) ++n;
for(int i = 0;i<n;i++) //求解LIS部分
{
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]);
}
cout<<ans<<endl;
for (int i = 0; i < n; i ++ )//求解贪心部分
{
int k = 0;
while(k<cnt && a[i] > g[k]) ++k; //g数组一定升序有序的,找到大于等于a[i]的最小的子序列结尾
g[k] = a[i];
if(k>=cnt) ++cnt; //没有就需要再开一个序列
}
cout<<cnt<<endl;
return 0;
}
187. 导弹防御系统
由于n=50较小,本题可以用上一题贪心+dfs暴搜
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 55;
int a[N],n;
int up[N];//表示所有上升子序列的末尾高度
int down[N];//表示所有下降子序列的末尾高度
int ans;
void dfs(int u,int cnt_u,int cnt_d) //u表示搜到第几个高度,cntu表示上升序列个数,cntd表示下降序列个数
{
if(cnt_u+cnt_d>=ans) return; //剪枝
if(u==n)//搜完了
{
ans = min(ans,cnt_u + cnt_d);//求最小值
return;
}
//情况1:将当前数放到上升子序列中
int k = 0;
while(k<cnt_u && a[u] <= up[k]) ++k;//找到小于a[u]的最大的上升子序列末尾
int t = up[k];//备份,用于恢复现场
up[k] = a[u];//放入a[u]
if(k<cnt_u) dfs(u+1,cnt_u,cnt_d);
else dfs(u+1,cnt_u+1,cnt_d);//没找到,那就重开一个序列
up[k] = t; //注意:恢复现场
//情况2:将当前数放到下降子序列中
k = 0;
while(k<cnt_d && a[u] >= down[k]) ++k;//找到大于a[u]的最小的下降子序列末尾
t = down[k];//备份,用于恢复现场
down[k] = a[u];//放入a[u]
if(k<cnt_d) dfs(u+1,cnt_u,cnt_d);
else dfs(u+1,cnt_u,cnt_d+1);//没找到,那就重开一个序列
down[k] = t;//注意:恢复现场
return;
}
int main()
{
while(cin>>n,n)
{
for(int i = 0;i<n;i++) cin>>a[i];
ans = 0x3f3f3f3f;//求最小值,初始化为无穷大
dfs(0,0,0);
cout<<ans<<endl;
}
return 0;
}
272. 最长公共上升子序列
状态表示:
f[i][j]代表所有a[1 ~ i]和b[1 ~ j]中以b[j]结尾的公共上升子序列的集合;
f[i][j]的值等于该集合的子序列中长度的最大值;
状态计算(对应集合划分):
首先依据公共子序列中是否包含a[i],将f[i][j]所代表的集合划分成两个不重不漏的子集:
不包含a[i]的子集,最大值是f[i - 1][j];
包含a[i]的子集,将这个子集继续划分,依据是子序列的倒数第二个元素在b[]中是哪个数:
子序列只包含b[j]一个数,长度是1;
子序列的倒数第二个数是b[1]的集合,最大长度是f[i - 1][1] + 1;
子序列的倒数第二个数是b[j - 1]的集合,最大长度是f[i - 1][j - 1] + 1;
如果直接按上述思路实现,需要三重循环:
朴素做法(O(n^3)超时)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 3010;
int a[N],b[N],n;
int f[N][N];//f[i][j]:所有由第一个序列的前i个字母,和第二个序列的前j个字母构成的,且以b[j]为结尾的公共上升子序列
int ans;
int main()
{
cin>>n;
for (int i = 1; i <= n; i ++ )
{
cin>>a[i];
}
for (int i = 1; i <= n; i ++ )
{
cin>>b[i];
}
for (int i = 1; i <= n; i ++ )
{
for (int j = 1; j <= n; j ++ )
{
f[i][j] = f[i-1][j];//所有不包含a[i]的公共上升子序列
if(a[i] == b[j])
{
f[i][j] = max(f[i][j],1); //求LIS
for(int k = 1;k<j;k++)
{
if(b[k] < b[j])
{
f[i][j] = max(f[i][j],f[i][k] + 1);
}
}
}
}
}
for (int i = 1; i <= n; i ++ ) //求最大值为答案
{
ans = max(ans,f[n][i]);
}
cout<<ans<<endl;
return 0;
}
然后我们发现每次循环求得的maxv是满足a[i] > b[k]的f[i - 1][k] + 1的前缀最大值。
因此可以直接将maxv提到第一层循环外面,减少重复计算,此时只剩下两重循环。
最终答案枚举子序列结尾取最大值即可。
优化做法(O(n^2))
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 3010;
int a[N],b[N],n;
int f[N][N];//f[i][j]:所有由第一个序列的前i个字母,和第二个序列的前j个字母构成的,且以b[j]为结尾的公共上升子序列
int ans;
int main()
{
cin>>n;
for (int i = 1; i <= n; i ++ )
{
cin>>a[i];
}
for (int i = 1; i <= n; i ++ )
{
cin>>b[i];
}
for (int i = 1; i <= n; i ++ )
{
int maxv = 1;
for(int j = 1;j<=n;j++)
{
f[i][j] = f[i-1][j];
if(a[i] == b[j]) f[i][j] = max(f[i][j],maxv);
if(a[i] > b[j]) maxv = max(maxv,f[i][j] + 1);
}
}
for (int i = 1; i <= n; i ++ ) //求最大值为答案
{
ans = max(ans,f[n][i]);
}
cout<<ans<<endl;
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)