OI学习笔记(二)
前言
本文为一名Oier的真实学习笔记,如果对文中有问题或有意见,欢迎联系我
本篇专门介绍 dp。
序列 dp
概念之类的就不说了。
例题:P1002 [NOIP 2002 普及组] 过河卒
- 思路先弱化问题,先不考虑马的存在。
由于每一次卒只能向下或者向右。
记从 (i,j) 出发,到达终点的路径条数为 \(f_{i,j}\)。
根据分类计数原理:
1. 往右走,可以到达 (\(i,j+1\))。
2. 往下走,可以到达 (\(i+1,j\))。
则得到递推关系式:\(f_{i,j}=f_{i,j+1}+f_{i+1,j}\)。
递推初值:\(f{n,m}=1\)。
由于需要根据较大行、较大列的 f 值推出较小行、较小列的 f 值,因此行和列需要逆序枚举。
#include<bits/stdc++.h>
using namespace std;
#define MAXN 110
#define ll long long
int dx[8]={2,1,-1,-2,-2,-1,1,2};
int dy[8]={1,2,2,1,-1,-2,-2,-1};
bool vis[MAXN][MAXN];
ll f[MAXN][MAXN];
int n,m,x,y;
int main(){
scanf("%d%d%d%d",&n,&m,&x,&y);
vis[x][y]=true;
for(int i=0;i<8;++i){
if(x+dx[i]>=0&&x+dx[i]<=n&&y+dy[i]>=0&&y+dy[i]<=m)
vis[x+dx[i]][y+dy[i]]=true;
}
f[n][m]=1;
for(int i=n;i>=0;i--)
for(int j=m;j>=0;j--){
if(i==n && j==m) continue;
if(vis[i][j])f[i][j]=0;
else f[i][j]=f[i+1][j]+f[i][j+1];
}
printf("%lld\n",f[0][0]);
return 0;
}
P1216 [IOI 1994 / USACO1.5] 数字三角形 Number Triangles
- 思路递推实现(动态规划)
int i,j;
for(j=1;j<=n;j++) d[n][j]=a[n][j];
for(i=n-1;i>=1;i--)
for(j=1;j<=i;j++)
d[i][j]=a[i][j]+max(d[i+1][j],d[i+1][j+1]);
时间复杂度:\(O(n^2)\)
使用动态规划(递推)的写法要保证 \(d_{i,j}\) 之前,已经计算出 \(d_{i+1,j}\) 和 \(d_{i+1,j+1}\)。
#include<bits/stdc++.h>
#define maxn 510
using namespace std;
int d[maxn][maxn],a[maxn][maxn];
int n;
int max(int a,int b){
return a>b?a:b;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
scanf("%d",&a[i][j]);
d[1][1]=a[1][1];
for(int i=2;i<=n;i++)
for(int j=1;j<=i;j++)
d[i][j]=max(d[i-1][j],d[i-1][j-1])+a[i][j];
int ans=0;
for(int i=1;i<=n;i++) ans=max(ans,d[n][i]);
printf("%d",ans);
return 0;
- LIS(最长上升子序列)问题
B3637 最长上升子序列 - 思路
建立一个数组 \(s_k\) 来储存所有长度为 \(k\) 的最长上升子序列的最后一个数字的最小值。即
用数学表达式写即为:\(s_k=min(b_j( F_j=k,1 \le j \le i))\)。
\(s_k\) 能发现什么性质?
\(s_k\) 是单调递增的!
定义 \(s[k]\) 表示 lis 长度为 \(k\) 的序列中,序列最后一个数字的最小值为 \(s[k]\)。
考虑使用反证法:如果 \(i<j\),而 \(s_i>s_j\):
由于长度为 j 的 lis 一定包含长度为 i 的情况,所以一定可以找到一个 \(m\),使得 \(m<s[j]\) ,且以 \(m\) 结尾的序列 lis 值为 \(i\)。
这与 lis 为 \(i\) 的序列中最后一个数字最小为 \(s_i\) 矛盾(因为 \(m\) 比 \(s_i\) 更小)。
单调性得证!
所以在求 \(f_i\) 值时,只需二分查找一个最大的 \(j\),使得 \(s_j<b_i\),则表示 \(b_i\) 可以跟在 \(s_j\) 后面,形成一个上升子序列,所以 \(f_i=j+1\)。
演示一下:
| i | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| \(b_i\) | 3 | 7 | 2 | 4 | 6 | 8 |
| \(F_i\) | 1 |
| k | 1 |
|---|---|
| \(s_k\) | 1 |
| i | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| \(b_i\) | 3 | 7 | 2 | 4 | 6 | 8 |
| \(F_i\) | 1 | 2 |
| k | 1 | 2 |
|---|---|---|
| \(s_k\) | 1 | 7 |
| i | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| \(b_i\) | 3 | 7 | 2 | 4 | 6 | 8 |
| \(F_i\) | 1 | 2 | 1 |
| k | 1 | 2 |
|---|---|---|
| \(s_k\) | 1 | 7 |
| i | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| \(b_i\) | 3 | 7 | 2 | 4 | 6 | 8 |
| \(F_i\) | 1 | 2 | 1 | 2 |
| k | 1 | 2 |
|---|---|---|
| \(s_k\) | 1 | 7 |
| i | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| \(b_i\) | 3 | 7 | 2 | 4 | 6 | 8 |
| \(F_i\) | 1 | 2 | 1 | 2 | 3 |
| k | 1 | 2 | 3 |
|---|---|---|---|
| \(s_k\) | 1 | 7 | 6 |
| i | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| \(b_i\) | 3 | 7 | 2 | 4 | 6 | 8 |
| \(F_i\) | 1 | 2 | 1 | 2 | 3 | 4 |
| k | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| \(s_k\) | 1 | 7 | 6 | 8 |
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll MAXN = 1000005;
int n,a[MAXN],dp[MAXN],R,l,r,ans;
int main(){
cin >> n;
for(int i = 1;i <= n;++i){
cin >> a[i];
}
dp[0]=0;
R=0;
for(int i=1;i<=n;++i){
if(a[i]>dp[R]){
dp[R+1]=a[i];
R++;
}else{
l=0;
r=R;
while (l<=r){
int mid = l + r>>1;
if (dp[mid] < a[i])l = mid+1;
else {
ans=mid;
r = mid-1;
}
}//循环结束后,r=l+1,l指向最右边一个小等于x的数,r指向最左边一个大于x的数。
dp[ans]=a[i];
}
}
int t = 0;
for(int i = 1;i <= n;++i){
if(dp[i]!=0)t++;
}
cout << t << endl;
return 0;
}
- 最优子结构
原问题最优,当且仅当子问题最优。
大问题的最优解可以由小问题的最优解推出,这个性质叫做最优子结构性质。 - DP 三连
设计 DP 算法,往往可以遵循 DP 三连:
我是谁? —— 设计状态,表示局面
我从哪里来?
我要到哪里去? —— 设计转移 - 如何学好 DP
未来将讲到 DP 的各种优化。
e.g. 数据结构优化、斜率优化。
一般而言,DP 的难点,在初学时是如何设计状态;在学习深入一些之后,变成了如何设计转移;在省选 / NOI 级别,又变成了如何设计状态。
学习 DP 主要靠做题练习。有一些设计状态的思想,需要在具体题目中总结。 - 线性(序列)模型
线性模型的是动态规划中最常用的模型,上例讲到的最长单调子序列就是经典的线性模型,这里的线性指的是状态的排布是呈线性的。
本类的状态是基础中的基础,大部分动态规划都要用到它,成为一个维。
常见的状态设计:
- \(f_i\) 表示前 \(i\) 个元素决策所形成的一个状态
- \(f_i[i]\) 表示用到了第 \(i\) 个元素,和其它在 \(1\) 到 \(i-1\) 间的元素,决策组成有的一个状态。
接下来再我们来看一道题:P1434 [SHOI2002] 滑雪
- 思路
发现这道题目看起来有点像 BFS,于是蒟蒻先想到了找出所有可能成为起点的地方,然后进行广搜直到无法向下滑了即可。写的代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int MAXN=105;
int ans,r,c,high;
int h[MAXN][MAXN];
int snow[MAXN][MAXN];
int dx[]={0,1,-0,-1};
int dy[]={-1,0,1,0};
queue<int>q;
void bfs(int x,int y){
q.push(x);
q.push(y);
while(!q.empty()){
int x=q.front();
q.pop();
int y=q.front();
q.pop();
for(int i=0;i<4;++i){//判断是否无法向下滑了
int nx=x+dx[i];
int ny=y+dy[i];
if(nx>0&&ny>0&&nx<=r&&ny<=c&&h[nx][ny]<h[x][y]){
snow[nx][ny]=max(snow[nx][ny],snow[x][y]+1);
q.push(nx);
q.push(ny);
}
}
}
}
signed main(){
scanf("%lld%lld",&r,&c);
for(int i=1;i<=r;++i)
for(int j=1;j<=c;++j){
scanf("%lld",&h[i][j]);
}
for(int i=1;i<=r;++i)
for(int j=1;j<=c;++j){
bool flag=false;
for(int k=0;k<4;++k)
if(i+dx[k]>0&&j+dy[k]>0&&i+dx[k]<=r&&j+dy[k]<=c&&h[i+dx[k]][j+dy[k]]>h[i][j])flag=true;
if(!flag){//找出滑雪的所有可能起点
snow[i][j]=1;
bfs(i,j);
}
}
for(int i=1;i<=r;++i)
for(int j=1;j<=c;++j)
ans=max(ans,snow[i][j]);
printf("%lld",ans);
return 0;
}
然后你满怀希望地交上去,发现竟然只拿了90 分!
注意到一个点可能会被加入到队列多次,导致队列膨胀,从而导致 MLE,因此可以这样修改:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=205;
int ans,r,c,high;
int h[MAXN][MAXN];
int snow[MAXN][MAXN];
int dx[]={0,1,0,-1};
int dy[]={-1,0,1,0};
queue<int>q;
void bfs(int x,int y){
q.push(x);
q.push(y);
while(!q.empty()){
int x=q.front(); q.pop();
int y=q.front(); q.pop();
for(int i=0;i<4;++i){
int nx=x+dx[i];
int ny=y+dy[i];
if(nx>0&&ny>0&&nx<=r&&ny<=c&&h[nx][ny]<h[x][y]){
if(snow[nx][ny] < snow[x][y]+1){ // 只有找到更长路径才继续
snow[nx][ny] = snow[x][y]+1;
q.push(nx);
q.push(ny);
}
}
}
}
}
int main(){
scanf("%d%d",&r,&c);
for(int i=1;i<=r;++i)
for(int j=1;j<=c;++j){
scanf("%d",&h[i][j]);
}
for(int i=1;i<=r;++i)
for(int j=1;j<=c;++j){
bool flag=false;
for(int k=0;k<4;++k)
if(h[i+dx[k]][j+dy[k]]>h[i][j])flag=true;
if(!flag){//找出滑雪的所有可能起点
snow[i][j]=1;
bfs(i,j);
}
}
for(int i=1;i<=r;++i)
for(int j=1;j<=c;++j)
ans=max(ans,snow[i][j]);
printf("%d",ans);
return 0;
}
一个经典问题:
题目描述
给定两个序列,求这两个序列的LCS长度.
LCS是 Longest Common Subsequence 的缩写,即最长公共子序列。
一个序列,如果同时是两个已知序列的子序列,且是所有子序列中最长的,则为最长公共子序列。
关于子序列举例说明:
\(1 2 3\) 的子序列有 \(8\) 个:
\(1\)
\(2\)
\(3\)
\(1 2\)
\(1 3\)
\(2 3\)
\(1 2 3\)
空序列
输入格式
第一行,一个整数n,表示第一个序列的长度;
第二行,n个整数,表示第一个序列;
第三行,一个整数m,表示第二个序列的长度;
第四行,m个整数,表示第二个序列;
输出格式
一个整数,表示所求得的LCS的长度。
输入输出样例 #1
输入 #1
4
1 2 3 4
4
1 3 2 4
输出 #1
3
说明/提示
两个序列的最长公共子序列为 \(1,2,4\) 或 \(1,3,4\)。长度为 \(3\)。
对于 \(100%\) 的测试数据,\(n,m \le 2000\)。
\(f{i,j}\) 表示第一个序列做到第 \(i\) 位,第二个序列做到第 \(j\) 位时的最长公共子序列。
if(a[i]!=b[j])
f[i][j]=max(f[i-1][j],f[i][j-1]);
else
f[i][j]=f[i-1][j-1]+1;
边界:f[i][0]=0,f[0][i]=0;
代码:
#include <bits/stdc++.h>
using namespace std;
int a[2010],b[2010],dp[2010][2010];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
int m;
cin>>m;
for(int i=1;i<=m;i++)cin>>b[i];
for (int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
if (a[i]==b[j]) dp[i][j]=dp[i-1][j-1]+1;
else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
cout<<dp[n][m];
return 0;
}
背包dp
-
01背包
这类问题是背包中最简单的问题,有 \(n\) 个物品,编号为 \(1~n\),其中第 \(i\) 个物品的价值是 \(v_i\) 重量是 \(w_i\)。有一个容量为 \(c\) 的背包,问选取哪些物品,可以使得在总重量不超过背包容量大的情况下,拿到的总价值最大。
通常为 dp[i][j]=max(拿,不拿)。即
先放道模板题:P1048 [NOIP 2005 普及组] 采药
- 思路
定义状态:\(dp_{i,j}\) 表示考虑前 \(i\) 种草药,且背包容量不超过 \(j\) 时的最大价值,容易得到状态转移方程:
代码如下:
#include<bits/stdc++.h>
using namespace std;
int t,m,w[105],v[105],dp[105][1005];
int main(){
cin >> m >> t;
for(int i = 1;i <= t;++i)scanf("%d%d",&w[i],&v[i]);
for(int i = 1;i <= t;++i){
for(int j = 0;j <= m;++j){
if(w[i] <= j){
dp[i][j] = max(dp[i - 1][j - w[i]] + v[i],dp[i - 1][j]);
}else dp[i][j] = dp[i - 1][j];
}
}
printf("%d\n",dp[t][m]);
return 0;
}
实际上,背包问题的时间复杂度已经没办法再优化了。
而空间复杂度还可以优化。在我们之前的算法中,01 背包的空间复杂度是 \(O(n \times m)\) 的。(用到一个二维数组,一维是 \(n\),一维是 \(m\))。
-
完全背包
和 01 背包不同的地方在于一个物品可以取无限次,状态转移方程通常为:
例题:B2174 完全背包
#include<bits/stdc++.h>
using namespace std;
int t,m,w[1005],v[1005],dp[1005][1005];
int main(){
cin >> t >> m;
for(int i = 1;i <= t;++i)scanf("%d%d",&w[i],&v[i]);
for(int i = 1;i <= t;++i){
for(int j = 0;j <= m;++j){
if(w[i] <= j){
dp[i][j] = max(dp[i][j - w[i]] + v[i],dp[i - 1][j]);
}else dp[i][j] = dp[i - 1][j];
}
}
printf("%d\n",dp[t][m]);
return 0;
}
-
多重背包
和 01 背包不同的地方在于一个物品可以取多次,并且最多取的次数已给出。
通常这样写:
for(int k=0;k<=t;++k){//选k件
if(j<k*p)break;//背包容量不足
dp[i][j]=max(dp[i-1][j,dp[i-1][j-k*p]+c*k);
}
例题:B2173 多重背包
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,v;
int p,c,t;
int dp[505][1005];
signed main(){
scanf("%lld%lld",&n,&v);
for(int i=1;i<=n;++i){
scanf("%lld%lld%lld",&p,&c,&t);
for(int j=0;j<=v;++j){
for(int k=0;k<=t;++k){//选k件
if(j<k*p)break;//背包容量不足
dp[i][j]=max(dp[i-1][j,dp[i-1][j-k*p]+c*k);
}
}
}
printf("%lld",dp[n][v]);
return 0;
}
-
分组背包
#include<bits/stdc++.h>
using namespace std;
int n,m;
struct Thing{
int i;//会场编号
int cost;//价格
int q;//魅力值
}a[10005];
int dp[1005];
int k;//最大的会场编号,便于遍历
vector<int>v[1005];//记录每组的物品编号
int main(){
cin>>m>>n;
for(int i=1;i<=n;++i)
{
scanf("%d%d%d",&a[i].cost,&a[i].q,&a[i].i);
k=max(k,a[i].i);//更新最大值
v[a[i].i].push_back(i);//把物品分组
}
for(int i=1;i<=k;++i)
{
for(int j=m;j>=0;--j)
for(int h=0;h<v[i].size();++h)
if(j>=a[v[i][h]].cost)
dp[j]=max(dp[j],dp[j-a[v[i][h]].cost]+a[v[i][h]].q);
}
printf("%d",dp[m]);
return 0;
}
留几道习题:
P5017 [NOIP 2018 普及组] 摆渡车
P2258 [NOIP 2014 普及组] 子矩阵
区间dp
以一道例题来说明:P1775 石子合并(弱化版)
区间动态规划问题一般都是考虑对于每段区间,他们的最优值都是由更小几段区间的最优值得到,是分治思想的一种应用,将一个区间问题不断划分为更小的区间直至一个元素组成的区间,枚举他们的组合 ,求合并后的最优值。
设 \(f_{i,j}(1\le i \le j \le n)\) 表示区间 \([i,j]\) 内的石子合并的最小代价
如何将 \(f_{i,j}\) 划分为更小的区间的一个子问题?
通过枚举最后一次合并石子的位置在第 \(k\) 个石子之后。
我们可以把第 \(i\) 堆到第 \(j\) 堆合并分为 \(3\) 步:
- 将 \([i,k]\) 中的石子合并为一堆。
- 将 \([k+1,j]\) 中的石子合并为一堆。
- 将两堆石子合并。
考虑初值?
\(dp_{i,i}=0(1\le i \le n)\)。
时间复杂度?
\(\mathcal{O}(n^3)\),可以通过。
// Author: heffo_hard
#include <bits/stdc++.h>
#define up(a,b,c) for(int (a)=(b);(a)<=(c);(a)=-~(a))
#define dn(a,b,c) for(int (a)=(b);(a)>=(c);(a)=~-(a))
#define fst first
#define sed second
#define pref static inline
#define gc() p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<20,stdin),p1==p2)?EOF:*p1++
using namespace std;
using hint = __int128;
using pii = pair<int, int>;
using us = unsigned short;
using ldb = long double;
using ll = long long;
using ull = unsigned long long;
using ui = unsigned int;
using pll = pair<ll, ll>;
using pil = pair<int, ll>;
using vpil = vector<pil>;
using vl = vector<ll>;
using pli = pair<ll, int>;
using vpli = vector<pli>;
using vi = vector<int>;
using vpi = vector<pii>;
using vpl = vector<pll>;
using db = double;
const int MAXN=305;
int n;
int x,pre[MAXN],f[MAXN][MAXN];
namespace mystl {
char buf[1 << 20],*p1 = buf,*p2 = buf, sr[1 << 23], z[23], nc;
int C =-1, Z = 0;
template<typename T>pref void read(T & x){
bool flag = false;
while (nc = gc(), (nc<48 || nc> 57) && nc !=-1) flag |= (nc == 45);
x = nc - 48;
while (nc = gc(), 47 < nc && nc < 58) x = (x << 3) + (x << 1) + (nc ^ 48);
if (flag) x = -x;
}
pref void read(char* s) {
char ch = gc();
while(ch <= 32) ch = gc();
int i = 0;
while(ch > 32) {
s[i++] = ch;
ch = gc();
}
s[i] = '\0';
}
pref void read(string &s) {
s.clear();
char ch = gc();
while(ch <= 32) ch = gc();
while(ch > 32) {
s += ch;
ch = gc();
}
}
pref void read(char &ch) {
ch = gc();
while(ch <= 32) ch = gc();
}
template<typename T, typename ... Args_Arrays_Typename_heffo_hard>
void read(T & x, Args_Arrays_Typename_heffo_hard & ...a){read(x); read(a...);}
pref void ot(){fwrite(sr, 1, C + 1, stdout ); C = -1;}
pref void flush(){if (C > 1 << 22) ot();}
template<typename T>pref void write(T x) {
if constexpr (is_same<T, char>::value) {
sr[++C] = x;
} else if constexpr (is_same<T, const char*>::value || is_same<T, char*>::value) {
for(int i = 0; x[i]; ++i) sr[++C] = x[i];
} else if constexpr (is_same<T, string>::value) {
for(char c : x) sr[++C] = c;
} else {
int y = 0;
if (x < 0) y = 1, x = -x;
Z = 0;
do {
z[++Z] = x % 10 + 48;
x /= 10;
} while (x);
if (y) z[++Z] = '-';
while (Z) sr[++C] = z[Z--];
}
flush();
}
template<typename T>pref void write(T x, char t) {
write(x);
sr[++C] = t;
flush();
}
pref void write(const char* s) {
for(int i = 0; s[i]; ++i) sr[++C] = s[i];
flush();
}
pref void write(string s) {
for(char c : s) sr[++C] = c;
flush();
}
pref ll qpow(ll a, ll b, ll p){
if (a == 0) return 0;
ll c = 1ll;
while (b){
if (b & 1) c = a * c % p;
a = a * a % p;
b >>= 1;
}
return c;
}
pref ll lcm(ll x, ll y){
return x / std:: __gcd(x, y) * y;
}
};
using namespace mystl;
namespace my {
constexpr int P = static_cast<int>(998244353);
pref void madd(int & x, int y){x = (x + y >= P) ? (x + y - P) : (x + y);}
pref int fmadd(int x, int y){return (x + y >= P) ? (x + y - P) : (x + y);}
pref void msub(int & x, int y){x = (x < y) ? (x - y + P) : (x - y);}
pref int fmsub(int x, int y){return (x < y) ? (x - y + P) : (x - y);}
pref void mmul(int & x, int y){x = (int)(1ll * x * y % P);}
pref int fmmul(int x, int y){return (int)(1ll * x * y % P);}
template<typename T>pref T min(T x, T y){return (x < y) ? (x) : (y);}
template<typename T>pref T max(T x, T y){return (x > y) ? (x) : (y);}
template<typename T>pref T abs(T x){return (x < 0) ? (-x) : (x);}
constexpr int N = static_cast<int>(0), inf = static_cast<int>(0x3f3f3f3f3f);
pref void solve(){
up(len,2,n){
for(int i=1;i+len-1<=n;++i){
int j=i+len-1;
for(int k=i;k<j;k++)f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+pre[j]-pre[i-1]);
}
}
write(f[1][n]);
}
}
int main(){
// freopen("","r",stdin);
// freopen("","w",stdout);
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
read(n);
memset(f,63,sizeof(f));
up(i,1,n){
f[i][i]=0;
read(x);
pre[i]=pre[i-1]+x;
}
my::solve();
ot();
return 0;
}
/*
*/

浙公网安备 33010602011771号