Loading

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 主要靠做题练习。有一些设计状态的思想,需要在具体题目中总结
  • 线性(序列)模型
    线性模型的是动态规划中最常用的模型,上例讲到的最长单调子序列就是经典的线性模型,这里的线性指的是状态的排布是呈线性的。
    本类的状态是基础中的基础,大部分动态规划都要用到它,成为一个维。
    常见的状态设计:
  1. \(f_i\) 表示前 \(i\) 个元素决策所形成的一个状态
  2. \(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(拿,不拿)。即

\[dp_{i,j}=\max(dp_{i-1,j-w_i}+v_i,dp_{i-1,j}) \]

先放道模板题:P1048 [NOIP 2005 普及组] 采药

  • 思路
    定义状态:\(dp_{i,j}\) 表示考虑前 \(i\) 种草药,且背包容量不超过 \(j\) 时的最大价值,容易得到状态转移方程:

\[dp_{i,j}=\max(dp_{i-1,j-w_i}+v_i,dp_{i-1,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 背包不同的地方在于一个物品可以取无限次,状态转移方程通常为:

\[dp_{i,j}=\max(dp_{i,j-w_i}+v_i,dp_{i-1,j}) \]

例题: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;
}
  • 分组背包

例题:P1757 通天之分组背包

#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\) 步:

  1. \([i,k]\) 中的石子合并为一堆。
  2. \([k+1,j]\) 中的石子合并为一堆。
  3. 将两堆石子合并。
    考虑初值?
    \(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;
}
/*

*/
posted @ 2026-04-25 21:54  heffo_hard  阅读(44)  评论(0)    收藏  举报