SCAU算法设计与分析 —— 动态规划

3 阅读23分钟

By 三石吖 2026.2

3.动态规划

8596 最长上升子序列

NN最大只有1e41e4,显然可以考虑O(n2)O(n^2)的做法

dp[i]dp[i]表示以第ii个元素结尾的最大子段和

递推:考虑每一个小于ii的位置jj,如果a[j]<a[i]a[j]<a[i],说明发现了一个上升段,转移方程为:dp[i]=max(dp[i],dp[j]+1)dp[i]=max(dp[i],dp[j]+1)

  • 时间复杂度:O(n2)O(n^2)
#include<bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using pii = std::pair<int,int>;
using pll = std::pair<i64,i64>;
using pli = std::pair<i64,int>;
using pil = std::pair<int,i64>;
constexpr i64 INF = 0x3f3f3f3f3f3f3f3fll;
constexpr int inf = 0x3f3f3f3f;

void solve(){
    int n;
    while(std::cin >> n){
        if(!n){
            break;
        }
        std::vector<int>a(n);
        for(int i = 0; i < n; i++){
            std::cin >> a[i];
        }
        std::vector<int>dp(n, 1);
        for(int i = 1; i < n; i++){
            for(int j = 0; j < i; j++){
                if(a[i] > a[j]){
                    dp[i] = std::max(dp[i], dp[j] + 1);
                }
            }
        }
        int ans = *std::max_element(dp.begin(), dp.end());
        std::cout << ans << '\n';
    }
}          

int main(){ 
    std::ios::sync_with_stdio(false);    
    std::cin.tie(nullptr);

#ifdef LOCAL
    freopen("make.txt", "r", stdin);
    freopen("a.txt", "w", stdout);
#endif

    int T = 1;
    // std::cin >> T;
    while(T--){
        solve();
    }
    return 0; 
}   
/* 

*/  

18708 最大子段和

考虑用dp[i]dp[i]表示以第ii个元素结尾的最大子段和

由于子段是连续的子数组,因此第ii个位置的状态只能由第i1i-1个位置推导而来

如果字段长度为1,那么就是a[i]a[i],否则是dp[i1]+a[i]dp[i-1]+a[i],于是有状态转移方程:dp[i]=max(dp[i1]+a[i],a[i])dp[i]=max(dp[i-1]+a[i],a[i])

若求最小子段和也是同理

  • 时间复杂度O(n)O(n)
#include<bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using pii = std::pair<int,int>;
using pll = std::pair<i64,i64>;
using pli = std::pair<i64,int>;
using pil = std::pair<int,i64>;
constexpr i64 INF = 0x3f3f3f3f3f3f3f3fll;
constexpr int inf = 0x3f3f3f3f;

void solve(){
    int n;
    std::cin >> n;
    std::vector<int>a(n);
    for(int i = 0; i < n; i++){
        std::cin >> a[i];
    }
    std::vector<int>dp(n);
    dp[0] = a[0];
    for(int i = 1; i < n; i++){
        dp[i] = std::max(dp[i - 1] + a[i], a[i]);
    }
    int ans = *std::max_element(dp.begin(), dp.end());
    std::cout << ans << "\n";
}          

int main(){ 
    std::ios::sync_with_stdio(false);    
    std::cin.tie(nullptr);

#ifdef LOCAL
    freopen("make.txt", "r", stdin);
    freopen("a.txt", "w", stdout);
#endif

    int T = 1;
    // std::cin >> T;
    while(T--){
        solve();
    }
    return 0; 
}   
/* 

*/  

19182 石子合并(基础版)

区间dpdp模板题

dp[i][j]dp[i][j]表示合并[i,j][i,j]这个区间石子的最小代价

一个区间的最小代价可以由更小的区间推出

形式化地,dp[i][j]=min(dp[i][j],dp[i][k] + dp[k+1][j] + ika[i]),ik<jdp[i][j] = min(dp[i][j], dp[i][k] \ + \ dp[k + 1][j] \ + \ \sum_{i}^{k}a[i]), \quad i \le k \lt j

然后枚举长度,枚举起点,递推即可

  • 时间复杂度O(n3)O(n^3)
#include<bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using pii = std::pair<int,int>;
using pll = std::pair<i64,i64>;
using pli = std::pair<i64,int>;
using pil = std::pair<int,i64>;
constexpr i64 INF = 0x3f3f3f3f3f3f3f3fll;
constexpr int inf = 0x3f3f3f3f;

void solve(){
    int n;
    std::cin >> n;
    std::vector<int>a(n + 1), pre(n + 1);
    for(int i = 1; i <= n; i++){
        std::cin >> a[i];
        pre[i] = pre[i - 1] + a[i];
    }
    std::vector<std::vector<int>>dp(n + 1, std::vector<int>(n + 1, inf));
    for(int i = 1; i <= n; i++){
        dp[i][i] = 0;
    }

    for(int len = 2; len <= n; len++){
        for(int i = 1; i + len - 1 <=n; i++){
            int j = i + len - 1;
            for(int k = i; k < j; k++){
                dp[i][j] = std::min(dp[i][j], dp[i][k] + dp[k + 1][j] + pre[j] - pre[i - 1]);
            }
        }
    }
    std::cout << dp[1][n] << "\n";
}             

int main(){ 
    std::ios::sync_with_stdio(false);    
    std::cin.tie(nullptr);

#ifdef LOCAL
    freopen("make.txt", "r", stdin);
    freopen("a.txt", "w", stdout);
#endif

    int T = 1;
    // std::cin >> T;
    while(T--){
        solve();
    }
    return 0; 
}   
/* 

*/  

8597 石子划分问题

  • 题目没说,实际上是可以排序的

dp[i][j]dp[i][j]表示在第ii个石子处,划分为jj份,且第ii个石子属于第jj份的最小代价

设上一份石子划分的结束位置为kk,则dp[i][j]=min(dp[i][j],dp[k][j1]+cost), cost=(a[i]a[k])×(a[i]a[k])dp[i][j]=min(dp[i][j],dp[k][j-1]+cost), \ cost=(a[i]-a[k]) \times (a[i]-a[k])

从前往后递推即可

  • 注意:第ii个石子处最多划分出min(i,m)min(i,m)

  • 时间复杂度O(n3)O(n^3)

#include<bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using pii = std::pair<int,int>;
using pll = std::pair<i64,i64>;
using pli = std::pair<i64,int>;
using pil = std::pair<int,i64>;
constexpr i64 INF = 0x3f3f3f3f3f3f3f3fll;
constexpr int inf = 0x3f3f3f3f;

void solve(){
    int n, m;
    std::cin >> n >> m;
    std::vector<int>a(n + 1);
    for(int i = 1; i <= n; i++){
        std::cin >> a[i];
    }
    std::sort(a.begin(), a.end());
    std::vector<std::vector<int>>dp(n + 1, std::vector<int>(m + 1, inf));
    dp[0][0] = 0;
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= std::min(m, i); j++){
            for(int k = 0; k < i; k++){
                int d = a[i] - a[k + 1];
                dp[i][j] = std::min(dp[i][j], dp[k][j - 1] + d * d);
            }
        }
    }
    std::cout << dp[n][m] << "\n";
}             

int main(){ 
    std::ios::sync_with_stdio(false);    
    std::cin.tie(nullptr);

#ifdef LOCAL
    freopen("make.txt", "r", stdin);
    freopen("a.txt", "w", stdout);
#endif

    int T = 1;
    // std::cin >> T;
    while(T--){
        solve();
    }
    return 0; 
}   
/* 

*/  

19185 01背包

dpdp入门题

dp[j]dp[j]表示容量为jj的背包可以装的最大价值

每次考虑是否加入新物品,递推公式:dp[j]=max(dp[j],dp[jw[i]]+v[i])dp[j]=max(dp[j],dp[j - w[i]]+v[i])

其中w[i],v[i]w[i],v[i]分别为第ii个物品的重量和价值

时间复杂度O(n×m)O(n \times m)

#include<bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using pii = std::pair<int,int>;
using pll = std::pair<i64,i64>;
using pli = std::pair<i64,int>;
using pil = std::pair<int,i64>;
constexpr i64 INF = 0x3f3f3f3f3f3f3f3fll;
constexpr int inf = 0x3f3f3f3f;

void solve(){
    int n,m;
    std::cin >> m >> n;
    std::vector<int>w(n), v(n);
    for(int i = 0; i < n; i++){
        std::cin >> w[i] >> v[i];
    }
    std::vector<int>dp(m + 1,0);
    for(int i = 0; i < n; i++){
        for(int j = m; j >= w[i]; j--){
            dp[j] = std::max(dp[j], dp[j - w[i]] + v[i]);
        }
    }
    std::cout << dp[m] <<"\n";
}          

int main(){ 
    std::ios::sync_with_stdio(false);    
    std::cin.tie(nullptr);

#ifdef LOCAL
    freopen("make.txt", "r", stdin);
    freopen("a.txt", "w", stdout);
#endif

    int T = 1;
    // std::cin >> T;
    while(T--){
        solve();
    }
    return 0; 
}   
/* 

*/  

9717 取数对弈

好题

dp[i][j]dp[i][j]表示从[i,j][i,j]区间内甲先手能取到最高分

甲第一步要么取a[i]a[i],要么取a[j]a[j]

①如果甲取a[i]a[i],变成乙先手取[i+1,j][i+1, j]内的数,由于总和固定且乙会按最优策略取,答案是k=ija[k]dp[i+1][j]\sum_{k=i}^{j}a[k]-dp[i+1][j]

②同理,如果甲取a[j]a[j],答案是k=ija[k]dp[i][j1]\sum_{k=i}^{j}a[k]-dp[i][j-1]

得到状态转移方程:dp[i][j]=k=ija[k]min(dp[i+1][j],dp[i][j1])dp[i][j]=\sum_{k=i}^{j}a[k]-min(dp[i+1][j],dp[i][j-1])

看着像是个区间dpdp,枚举长度后递推即可

  • 时间复杂度O(n2)O(n^2)
#include<bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using pii = std::pair<int,int>;
using pll = std::pair<i64,i64>;
using pli = std::pair<i64,int>;
using pil = std::pair<int,i64>;
constexpr i64 INF = 0x3f3f3f3f3f3f3f3fll;
constexpr int inf = 0x3f3f3f3f;

void solve(){
    int n;
    std::cin >> n;
    std::vector<int>a(n + 1), pre(n + 1);
    for(int i = 1; i <= n; i++){
        std::cin >> a[i];
        pre[i] = pre[i - 1] + a[i];
    }
    std::vector<std::vector<int>>dp(n + 1, std::vector<int>(n + 1));
    for(int i = 1; i <= n; i++){
        dp[i][i] = a[i];
    }

    for(int len = 2; len <= n; len++){
        for(int i = 1; i + len - 1 <= n; i++){
            int j = i + len - 1;
            dp[i][j] = std::max(dp[i][j], pre[j] - pre[i - 1] - std::min(dp[i + 1][j], dp[i][j - 1]));
        }
    }

    std::cout << dp[1][n] << " " << pre[n] - dp[1][n] << "\n";
}             

int main(){ 
    std::ios::sync_with_stdio(false);    
    std::cin.tie(nullptr);

#ifdef LOCAL
    freopen("make.txt", "r", stdin);
    freopen("a.txt", "w", stdout);
#endif

    int T = 1;
    // std::cin >> T;
    while(T--){
        solve();
    }
    return 0; 
}   
/* 

*/  

10303 数字三角

dp[i][j]dp[i][j]表示到达第ii行第jj列的最大路径和

对于每个位置,都有:dp[i][j]=dp[i1][j]+a[i][j]dp[i][j] = dp[i-1][j] + a[i][j]

如果j>0j \gt 0dp[i][j]=min(dp[i][j],dp[i1][j1]+a[i][j])dp[i][j] = min(dp[i][j],dp[i-1][j-1]+a[i][j])

至于路径只需要倒着找一遍即可

#include<bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using pii = std::pair<int,int>;
using pll = std::pair<i64,i64>;
using pli = std::pair<i64,int>;
using pil = std::pair<int,i64>;
constexpr i64 INF = 0x3f3f3f3f3f3f3f3fll;
constexpr int inf = 0x3f3f3f3f;

void solve(){
    int n;
    std::cin >> n;
    std::vector<std::vector<int>>a(n);
    for(int i = 0; i < n; i++){
        a[i].resize(i + 1);
        for(int j = 0; j <= i; j++){
            std::cin >> a[i][j];
        }
    }
    std::vector<std::vector<int>>dp(n, std::vector<int>(n, 0));
    dp[0][0] = a[0][0];
    for(int i = 1; i < n; i++){
        for(int j = 0; j <= i; j++){
            dp[i][j] = std::max(dp[i][j], dp[i - 1][j] + a[i][j]);
            if(j){
                dp[i][j] = std::max(dp[i][j], dp[i - 1][j - 1] + a[i][j]);
            }
        }
    }
    std::vector<int>path;
    int max = 0, r = n - 1, c;
    for(int i = 0; i < n; i++){
        if(dp[n - 1][i] >= max){
            max = dp[n - 1][i];
            c = i;
        }
    }
    while(r > 0){
        path.push_back(a[r][c]);
        if(dp[r - 1][c] != dp[r][c] - a[r][c]){
            c--;
        }
        r--;
    }
    path.push_back(a[0][0]);
    std::cout << max << "\n";
    for(int i = path.size() - 1; i >= 0; i--){
        std::cout << path[i] << " \n"[i == 0];
    }
}             

int main(){ 
    std::ios::sync_with_stdio(false);    
    std::cin.tie(nullptr);

#ifdef LOCAL
    freopen("make.txt", "r", stdin);
    freopen("a.txt", "w", stdout);
#endif

    int T = 1;
    // std::cin >> T;
    while(T--){
        solve();
    }
    return 0; 
}   
/* 

*/  

10349 数字滑雪

dp[i][j]dp[i][j]为以a[i][j]a[i][j]为起点的最长路径,然后往四个方向进行深搜

发现,有可能会访问到以前已经搜过的节点,此时不用继续搜索,直接相加后返回即可,这就是记忆化搜索

  • 每个位置访问一遍,时间复杂度O(n×m)O(n \times m)
#include<bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using pii = std::pair<int,int>;
using pll = std::pair<i64,i64>;
using pli = std::pair<i64,int>;
using pil = std::pair<int,i64>;
constexpr i64 INF = 0x3f3f3f3f3f3f3f3fll;
constexpr int inf = 0x3f3f3f3f;

constexpr int dx[] = {1, -1, 0, 0};
constexpr int dy[] = {0, 0, -1, 1};
void solve(){
    int n, m;
    std::cin >> n >> m;
    std::vector<std::vector<int>>a(n, std::vector<int>(m));
    for(int i = 0; i < n; i++){
        for(int j = 0; j < m; j++){
            std::cin >> a[i][j];
        }
    }
    std::vector<std::vector<int>>dp(n, std::vector<int>(m));

    auto dfs = [&] (auto &&self, int x, int y) -> int {
        if(dp[x][y]){
            return dp[x][y];
        }

        dp[x][y] = 1;
        for(int i = 0; i < 4; i++){
            int r = x + dx[i];
            int c = y + dy[i];
            if(r >= 0 && r < n && c >= 0 && c < m && a[r][c] < a[x][y]){
                dp[x][y] = std::max(dp[x][y], 1 + self(self, r, c));
            } 
        }
        return dp[x][y];
    };

    int ans = 0;
    for(int i = 0; i < n; i++){
        for(int j = 0; j < m; j++){
            ans = std::max(ans, dfs(dfs, i, j));
        }
    }
    std::cout << ans << "\n";
}             

int main(){ 
    std::ios::sync_with_stdio(false);    
    std::cin.tie(nullptr);

#ifdef LOCAL
    freopen("make.txt", "r", stdin);
    freopen("a.txt", "w", stdout);
#endif

    int T = 1;
    // std::cin >> T;
    while(T--){
        solve();
    }
    return 0; 
}   
/* 

*/  

11077 最长公共子字符串

好像不是很好理解!

设两个字符串分别为aabb

dp[i][j]dp[i][j]不为00,则表示aa的前ii位和bb的前jj位在末尾对齐,且最后一位相同情况下的最长公共子串长度

若为00表示末尾对齐时最后一位不匹配

这么解释的话,显然是只有a[i1]=b[j1]a[i-1]=b[j-1]时才进行状态转移(索引从00开始)

dp[i][j]=dp[i1][j1]+1dp[i][j]=dp[i-1][j-1]+1

#include<bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using pii = std::pair<int,int>;
using pll = std::pair<i64,i64>;
using pli = std::pair<i64,int>;
using pil = std::pair<int,i64>;
constexpr i64 INF = 0x3f3f3f3f3f3f3f3fll;
constexpr int inf = 0x3f3f3f3f;

void solve(){
    std::string a, b;
    std::cin >> a >> b;
    int n = a.size(), m = b.size();
    std::vector<std::vector<int>>dp(n + 1, std::vector<int>(m + 1));

    int max = 0, inx = 0;
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            if(a[i - 1] != b[j - 1]){
                continue;
            }
            dp[i][j] = dp[i - 1][j - 1] + 1;
            if(dp[i][j] > max){
                max = dp[i][j];
                inx = i - dp[i][j];
            }
        }
    }
    std::string ans = a.substr(inx, max);
    std::cout << max << "\n" << ans << "\n";
}             

int main(){ 
    std::ios::sync_with_stdio(false);    
    std::cin.tie(nullptr);

#ifdef LOCAL
    freopen("make.txt", "r", stdin);
    freopen("a.txt", "w", stdout);
#endif

    int T = 1;
    // std::cin >> T;
    while(T--){
        solve();
    }
    return 0; 
}   
/* 

*/  

11078 不能移动的石子合并

环形的石子合并可以转化为一排的石子合并,我们往数组后面按序重新填充这nn个元素后做dpdp

dpdp逻辑和一排的情况相同

相当于是枚举所有nn个位置作为起点进行排形dpdp后取最优解

#include<bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using pii = std::pair<int,int>;
using pll = std::pair<i64,i64>;
using pli = std::pair<i64,int>;
using pil = std::pair<int,i64>;
constexpr i64 INF = 0x3f3f3f3f3f3f3f3fll;
constexpr int inf = 0x3f3f3f3f;

void solve(){
    int n;
    std::cin >> n;
    std::vector<int>a(n + 1);
    std::vector<i64>pre(2 * n + 1, 0);
    for(int i = 1; i <= n; i++){
        std::cin >> a[i];
        pre[i] = pre[i - 1] + a[i];
    }
    for(int i = n + 1; i <= 2 * n; i++){
        a.push_back(a[i - n]);
        pre[i] = pre[i - 1] + a[i];
    }
    auto calc = [&](int len){
        std::vector<std::vector<i64>>dp1(len + 1, std::vector<i64>(len + 1, INF));
        std::vector<std::vector<i64>>dp2(len + 1, std::vector<i64>(len + 1, 0));
        for(int i = 1; i <= len; i++){
            dp1[i][i] = 0;
        }
        for(int i = len; i; i--){
            for(int j = i + 1; j <= len; j++){
                for(int k = i; k < j; k++){
                    dp1[i][j] = std::min(dp1[i][j], dp1[i][k] + dp1[k + 1][j] + pre[j] - pre[i - 1]);
                    dp2[i][j] = std::max(dp2[i][j], dp2[i][k] + dp2[k + 1][j] + pre[j] - pre[i - 1]);
                }
            }
        }
        i64 ans1 = INF, ans2 = 0;
        for(int i = 1; i + n - 1 <= len; i++){
            ans1 = std::min(ans1, dp1[i][i + n - 1]);
            ans2 = std::max(ans2, dp2[i][i + n - 1]);
        }
        std::cout << ans1 << " " << ans2 << "\n";
    };
    calc(n), calc(2 * n);
}          

int main(){ 
    std::ios::sync_with_stdio(false);    
    std::cin.tie(nullptr);

#ifdef LOCAL
    freopen("make.txt", "r", stdin);
    freopen("a.txt", "w", stdout);
#endif

    int T = 1;
    // std::cin >> T;
    while(T--){
        solve();
    }
    return 0; 
}   
/* 

*/  

11080 游泳圈的最大子矩阵和

数据范围极小,可以直接枚举矩形的长度和高度

为了简化求和,先用二位前缀和预处理

由于是环形的,我们可以在每一行后按序填充这一行的mm个元素,然后在nn行以后重新按需填充nn行,形成一个2n×2m2n \times 2m的数组,然后枚举即可

  • 复杂度O(n2×m2)O(n^2 \times m^2)
  • 可以让行和列下标从11开始,方便处理前缀和
#include<bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using pii = std::pair<int,int>;
using pll = std::pair<i64,i64>;
using pli = std::pair<i64,int>;
using pil = std::pair<int,i64>;
constexpr i64 INF = 0x3f3f3f3f3f3f3f3fll;
constexpr int inf = 0x3f3f3f3f;

void solve(){
    int n, m;
    std::cin >> n >> m;
    std::vector<std::vector<int>>a(n + 1, std::vector<int>(m + 1));
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            std::cin >> a[i][j];
            a[i].push_back(a[i][j]);
        }
        a.push_back(a[i]);
    }
    std::vector<std::vector<int>>pre(2 * n + 1, std::vector<int>(2 * m + 1));
    for(int i = 1; i <= 2 * n; i++){
        for(int j = 1; j <= 2 * m; j++){
            pre[i][j] = pre[i - 1][j] + pre[i][j - 1] - pre[i - 1][j - 1] + a[i][j];
        }
    }
    int ans = -inf;
    for(int h = 1; h <= n; h++){
        for(int w = 1; w <= m; w++){
            for(int i = 1; i + h - 1 <= 2 * n; i++){
                for(int j = 1; j + w - 1 <= 2 * m; j++){
                    int tmp = pre[i + h - 1][j + w - 1] - pre[i - 1][j + w - 1] - pre[i + h - 1][j - 1] + pre[i - 1][j - 1];
                    ans = std::max(ans, tmp);
                }
            }
        }
    }
    std::cout << ans << "\n";
}             

int main(){ 
    std::ios::sync_with_stdio(false);    
    std::cin.tie(nullptr);

#ifdef LOCAL
    freopen("make.txt", "r", stdin);
    freopen("a.txt", "w", stdout);
#endif

    int T = 1;
    // std::cin >> T;
    while(T--){
        solve();
    }
    return 0; 
}   
/* 

*/  

11083 旅游背包

dp[x][y]dp[x][y]表示体积为xx,重量为yy时能达到的最大价值

由于每个物品有多个,因此对于一个物品来说,可以进行c[i]c[i]次状态转移

状态转移方程:dp[x][y]=max(dp[x][y],dp[xv[i]][yw[i]]+t[i])dp[x][y]=max(dp[x][y],dp[x-v[i]][y-w[i]]+t[i])

  • 时间复杂度O(V×W×i=1nc[i])O(V \times W \times \sum_{i=1}^{n}c[i])
#include<bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using pii = std::pair<int,int>;
using pll = std::pair<i64,i64>;
using pli = std::pair<i64,int>;
using pil = std::pair<int,i64>;
constexpr i64 INF = 0x3f3f3f3f3f3f3f3fll;
constexpr int inf = 0x3f3f3f3f;

void solve(){
    int n, V, W;
    std::cin >> n >> V >> W;
    std::vector<int>v(n), w(n), c(n), t(n);
    for(int i = 0; i < n; i++){
        std::cin >> v[i] >> w[i] >> c[i] >> t[i];
    }

    std::vector<std::vector<int>>dp(V + 1, std::vector<int>(W + 1));
    for(int i = 0; i < n; i++){
        for(int k = 0; k < c[i]; k++){
            for(int vv = V; vv >= v[i]; vv--){
                for(int ww = W; ww >= w[i]; ww--){
                    dp[vv][ww] = std::max(dp[vv][ww], dp[vv - v[i]][ww - w[i]] + t[i]);
                }
            }
        }
    }
    std::cout << dp[V][W] << "\n";
}             

int main(){ 
    std::ios::sync_with_stdio(false);    
    std::cin.tie(nullptr);

#ifdef LOCAL
    freopen("make.txt", "r", stdin);
    freopen("a.txt", "w", stdout);
#endif

    int T = 1;
    // std::cin >> T;
    while(T--){
        solve();
    }
    return 0; 
}   
/* 

*/  

11090 最大mm段乘积和最小mm段和

法一:动态规划

首先可以预处理出[i,j][i,j]段表示的数,设为num[i][j]num[i][j],由于数据范围很小,直接O(n2)O(n^2)暴力即可

①最大mm段乘积:用dp[i][j]dp[i][j]表示前ii个位置划分出jj段,且第ii个位置属于第jj段的最大乘积

状态转移就是往前找第j1j-1段的结束位置,dp[i][j]=max(dp[i][j],dp[k][j1]×num[k+1][j]), k<jdp[i][j]=max(dp[i][j],dp[k][j-1] \times num[k+1][j]),\ k \lt j

②最小mm​段和:用dp[i][j]dp[i][j]表示前ii个位置划分出jj段,且第ii个位置属于第jj段的最小和

状态转移同样是往前找第j1j-1段的结束位置,dp[i][j]=min(dp[i][j],dp[k][j1]+num[k+1][j]), k<jdp[i][j]=min(dp[i][j],dp[k][j-1] + num[k+1][j]),\ k \lt j

至于找表达式,倒着找一遍即可

  • 时间复杂度O(n2×m)O(n^2 \times m)
#include<bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using pii = std::pair<int,int>;
using pll = std::pair<i64,i64>;
using pli = std::pair<i64,int>;
using pil = std::pair<int,i64>;
constexpr i64 INF = 0x3f3f3f3f3f3f3f3fll;
constexpr int inf = 0x3f3f3f3f;

void solve(){
    int n, m;
    std::string s;
    std::cin >> n >> m >> s;
    s = " " + s;
    std::vector<std::vector<i64>>num(n + 1, std::vector<i64>(n + 1));
    for(int i = 1; i <= n; i++){
        i64 cur = 0;
        for(int j = i; j <= n; j++){
            cur = cur * 10 + (s[j] - '0');
            num[i][j] = cur;
        }
    }
    std::vector<std::vector<i64>>dp1(n + 1, std::vector<i64>(m + 1, 0));
    std::vector<std::vector<i64>>dp2(n + 1, std::vector<i64>(m + 1, INF));
    dp1[0][0] = 1, dp2[0][0] = 0;
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= std::min(m, i); j++){
            for(int k = 0; k < i; k++){
                dp1[i][j] = std::max(dp1[i][j], dp1[k][j - 1] * num[k + 1][i]);
                dp2[i][j] = std::min(dp2[i][j], dp2[k][j - 1] + num[k + 1][i]);
            }
        }
    }
    std::cout << dp1[n][m] << " " << dp2[n][m] << "\n";
    std::vector<i64>path1, path2;
    int inx = n, x = m;
    while(x){
        int nxt;
        for(int i = x - 1; i < inx; i++){
            if(dp1[i][x - 1] * num[i + 1][inx] == dp1[inx][x]){
                nxt = i;
                break;
            }
        }
        path1.push_back(num[nxt + 1][inx]);
        inx = nxt, x--;
    }
    std::reverse(path1.begin(), path1.end());

    inx = n, x = m;
    while(x){
        int nxt;
        for(int i = x - 1; i < inx; i++){
            if(dp2[i][x - 1] + num[i + 1][inx] == dp2[inx][x]){
                nxt = i;
                break;
            }
        }
        path2.push_back(num[nxt + 1][inx]);
        inx = nxt, x--;
    }
    std::reverse(path2.begin(), path2.end());
    for(int i = 0; i < m; i++){
        std::cout << path1[i] << "*="[i == m - 1];
    }
    std::cout << dp1[n][m] << "\n";

    for(int i = 0; i < m; i++){
        std::cout << path2[i] << "+="[i == m - 1];
    }
    std::cout << dp2[n][m] << "\n";
}             

int main(){ 
    std::ios::sync_with_stdio(false);    
    std::cin.tie(nullptr);

#ifdef LOCAL
    freopen("make.txt", "r", stdin);
    freopen("a.txt", "w", stdout);
#endif

    int T = 1;
    // std::cin >> T;
    while(T--){
        solve();
    }
    return 0; 
}   
/* 

*/  

法二:搜索

数据范围只有1010,当然可以搜索

首先确定参数:我们需要知道前面哪些位置已经被用掉了,所以需要索引;要知道当前的和/积;还要知道前面已经划分出了多少段

确定参数后就是返回条件的确定:最多划分mm段,在最后一段划分的时候就应该更新答案并且返回了

进一步地,如果第m1m-1段已经划分完了,剩下部分必然属于最后一段,于是可以直接在划分出m1m-1段就更新答案并且返回

下面的代码中inxinx是当前段开始的索引,curcur是当前已有的和/积,havhav是在inxinx这个位置前划分出了几段

  • 时间复杂度:首先预处理部分是O(n2)O(n^2)的,搜索部分是划分出mm个非空段,用隔板法计算,一共有(n1m1)m\binom{n-1}{m-1} \cdot m种,每次拷贝的开销都是O(m)O(m),共O((n1m1)m)O\left(\binom{n-1}{m-1} \cdot m\right),总复杂度O(n2+((n1m1)m)O(n^2+(\binom{n-1}{m-1} \cdot m)
#include<bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using pii = std::pair<int,int>;
using pll = std::pair<i64,i64>;
using pli = std::pair<i64,int>;
using pil = std::pair<int,i64>;
constexpr i64 INF = 0x3f3f3f3f3f3f3f3fll;
constexpr int inf = 0x3f3f3f3f;

void solve(){
    int n, m;
    std::string s;
    std::cin >> n >> m >> s;
    std::vector<std::vector<i64>>num(n, std::vector<i64>(n));
    for(int i = 0; i < n; i++){
        i64 cur = 0;
        for(int j = i; j < n; j++){
            cur = cur * 10 + (s[j] - '0');
            num[i][j] = cur;
        }
    }

    std::vector<i64>path1, path2, p;
    i64 max = -1, min = INF;
    auto dfs1 = [&](auto &&self, int inx, i64 cur, int hav) -> void {
        if(hav == m - 1){
            if(cur * num[inx][n - 1] > max){
                max = cur * num[inx][n - 1];
                path1 = p;
                path1.push_back(num[inx][n - 1]);
            }
            return;
        }

        for(int i = inx; n - i >= m - hav; i++){
            p.push_back(num[inx][i]);
            self(self, i + 1, cur * num[inx][i], hav + 1);
            p.pop_back();
        }
    };
    dfs1(dfs1, 0, 1, 0);

    auto dfs2 = [&](auto &&self, int inx, i64 cur, int hav) -> void {
        if(hav == m - 1){
            if(cur + num[inx][n - 1] < min){
                min = cur + num[inx][n - 1];
                path2 = p;
                path2.push_back(num[inx][n - 1]);
            }
            return;
        }

        for(int i = inx; n - i >= m - hav; i++){
            p.push_back(num[inx][i]);
            self(self, i + 1, cur + num[inx][i], hav + 1);
            p.pop_back();
        }
    };
    dfs2(dfs2, 0, 0, 0);

    std::cout << max << " " << min << "\n";
    for(int i = 0; i < m; i++){
        std::cout << path1[i] << "*="[i == m - 1];
    }
    std::cout << max << "\n";
    for(int i = 0; i < m; i++){
        std::cout << path2[i] << "+="[i == m - 1];
    }
    std::cout << min << "\n";
}             

int main(){ 
    std::ios::sync_with_stdio(false);    
    std::cin.tie(nullptr);

#ifdef LOCAL
    freopen("make.txt", "r", stdin);
    freopen("a.txt", "w", stdout);
#endif

    int T = 1;
    // std::cin >> T;
    while(T--){
        solve();
    }
    return 0; 
}   
/* 

*/  

8595 钱币组合问题

多重背包问题,相较0101背包多了数量上的限制

  • 注意循环的顺序问题,寻找构成方法数其实相当于是找组合数而不是排列数
  • 先拿一张11分再拿两张11分和一次拿三张11分是同种情况!
  • 时间复杂度O(i=1nki×m)O(\sum_{i=1}^{n}k_i \times m),最劣50×100×20000=1e750 \times 100 \times 20000 = 1e7
#include<bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using pii = std::pair<int,int>;
using pll = std::pair<i64,i64>;
using pli = std::pair<i64,int>;
using pil = std::pair<int,i64>;
constexpr i64 INF = 0x3f3f3f3f3f3f3f3fll;
constexpr int inf = 0x3f3f3f3f;

void solve(){
    int n;
    std::cin >> n;
    std::vector<int>v(n), k(n);
    for(int i = 0; i < n; i++){
        std::cin >> v[i];
    }
    for(int i = 0; i < n; i++){
        std::cin >> k[i];
    }   
    int m;
    std::cin >> m;
    std::vector<int>dp1(m + 1), dp2(m + 1, inf);
    dp1[0] = 1, dp2[0] = 0;
    for(int i = 0; i < n; i++){
        for(int j = m; j > 0; j--){
            for(int t = 1; t <= k[i]; t++){
                if(j < t * v[i]){
                    break;
                }
                dp1[j] += dp1[j - t * v[i]];
            }
        }
    }

    for(int i = 0; i < n; i++){
        for(int t = 1; t <= k[i]; t++){
            for(int j = m; j >= v[i]; j--){
                dp2[j] = std::min(dp2[j], dp2[j - v[i]] + 1);
            }
        }
    }
    if(!dp1[m]){
        std::cout << 0 << "\n" << "no possible\n";
    }else{
        std::cout << dp1[m] << "\n" << dp2[m] << "\n";
    }
}             

int main(){ 
    std::ios::sync_with_stdio(false);    
    std::cin.tie(nullptr);

#ifdef LOCAL
    freopen("make.txt", "r", stdin);
    freopen("a.txt", "w", stdout);
#endif

    int T = 1;
    // std::cin >> T;
    while(T--){
        solve();
    }
    return 0; 
}   
/* 

*/  

17089最大mm子段和

好像很困难!

dp[i][j]dp[i][j]表示到第ii位置,已经划分出jj段,且a[i]a[i]属于第jj段时的最大子段和

①如果第i1i-1位置时已经划分出了jj段,那么直接加入a[i]a[i],即为dp[i][j]=dp[i1][j]+a[i]dp[i][j]=dp[i-1][j]+a[i]

②如果是在ii位置才开始第jj段呢,此时维护一个长度为mmprepre数组,pre[k]pre[k]用于表示[0,i1][0,i-1]划分出了kk段时的最大子段和,那么有dp[i][j]=pre[j1]+a[i]dp[i][j]=pre[j-1]+a[i]

于是得到状态转移方程:dp[i][j]=min(dp[i1][j]+a[i],pre[j1]+a[i])dp[i][j]=min(dp[i-1][j]+a[i],pre[j-1]+a[i])

当原数组中非正数m\ge m时,可以让答案和00maxmax

  • 时间复杂度O(n×m)O(n \times m)
  • 该题数据极弱,实际数据nn不超过9090mm不超过3030,写成O(n2×m)O(n^2 \times m)都能过
#include<bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using pii = std::pair<int,int>;
using pll = std::pair<i64,i64>;
using pli = std::pair<i64,int>;
using pil = std::pair<int,i64>;
constexpr i64 INF = 0x3f3f3f3f3f3f3f3fll;
constexpr int inf = 0x3f3f3f3f;

void solve(){
    int n, m;
    std::cin >> n >> m;
    std::vector<int>a(n);
    int cnt = 0;
    for(int i = 0; i < n; i++){
        std::cin >> a[i];
        cnt += (a[i] <= 0);
    }
    std::vector<std::vector<int>>dp(n, std::vector<int>(m + 1, -inf));
    std::vector<int>pre(m + 1, -inf);
    dp[0][1] = pre[1] = a[0];
    for(int i = 1; i < n; i++){
        for(int j = 1; j <= std::min(i + 1, m); j++){
            dp[i][j] = std::max(dp[i - 1][j] + a[i], pre[j - 1] + a[i]);
        }
        
        for(int j = 1; j <= std::min(i + 1, m); j++){
            pre[j] = std::max(pre[j], dp[i][j]);
        }
    }
    int ans = -inf;
    for(int i = m - 1; i < n; i++){
        ans = std::max(ans, dp[i][m]);
    }
    if(cnt >= m){
        ans = std::max(ans, 0);
    }
    std::cout << ans << "\n";
}             

int main(){ 
    std::ios::sync_with_stdio(false);    
    std::cin.tie(nullptr);

#ifdef LOCAL
    freopen("make.txt", "r", stdin);
    freopen("a.txt", "w", stdout);
#endif

    int T = 1;
    // std::cin >> T;
    while(T--){
        solve();
    }
    return 0; 
}   
/* 

*/  

17098 广告牌最佳安放问题

首先按照大小对广告牌位置排序,然后维护一个和当前广告牌距离>5\gt5的最大值,每更新一个位置双指针一下即可

实际上给的数据都是有序的,不用排序,那直接双指针跑一遍即可

  • 时间复杂度O(n)O(n)
#include<bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using pii = std::pair<int,int>;
using pll = std::pair<i64,i64>;
using pli = std::pair<i64,int>;
using pil = std::pair<int,i64>;
constexpr i64 INF = 0x3f3f3f3f3f3f3f3fll;
constexpr int inf = 0x3f3f3f3f;

void solve(){
    int m, n;
    std::cin >> m >> n;
    std::vector<int>x(n), r(n);
    for(int i = 0; i < n; i++){
        std::cin >> x[i];
    }
    for(int i = 0; i < n; i++){
        std::cin >> r[i];
    }
    std::vector<int>dp(n);
    int max = 0;
    for(int i = 0, j = 0; i < n; i++){
        while(x[i] - x[j] > 5){
            max = std::max(max, dp[j]);
            j++;
        }
        dp[i] = max + r[i];
    }
    int ans = *std::max_element(dp.begin(), dp.end());
    std::cout << ans << "\n";
}             

int main(){ 
    std::ios::sync_with_stdio(false);    
    std::cin.tie(nullptr);

#ifdef LOCAL
    freopen("make.txt", "r", stdin);
    freopen("a.txt", "w", stdout);
#endif

    int T = 1;
    // std::cin >> T;
    while(T--){
        solve();
    }
    return 0; 
}   
/* 

*/  

17099 周工作计划安排

  • 他这里应该是默认第11周不能接高压工作

dp[i][0]dp[i][0]表示第ii周接低压工作的最大收益,dp[i][1]dp[i][1]表示第ii周接高压工作的最大收益

容易得到状态转移方程:

dp[i][0]=max(dp[i1][0],dp[i1][1])+l[i]dp[i][0]=max(dp[i-1][0],dp[i-1][1])+l[i]

dp[i][1]=max(dp[i2][0],dp[i2][1])+h[i]dp[i][1]=max(dp[i-2][0],dp[i-2][1])+h[i]

  • 时间复杂度O(n)O(n)
#include<bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using pii = std::pair<int,int>;
using pll = std::pair<i64,i64>;
using pli = std::pair<i64,int>;
using pil = std::pair<int,i64>;
constexpr i64 INF = 0x3f3f3f3f3f3f3f3fll;
constexpr int inf = 0x3f3f3f3f;

void solve(){
    int n;
    std::cin >> n;
    std::vector<int>l(n + 1), h(n + 1);
    for(int i = 1; i <= n; i++){
        std::cin >> l[i];
    }
    for(int i = 1; i <= n; i++){
        std::cin >> h[i];
    }

    std::vector<std::vector<int>>dp(n + 1, std::vector<int>(2));
    dp[1][0] = l[1];
    for(int i = 2; i <= n; i++){
        dp[i][0] = std::max(dp[i - 1][0], dp[i - 1][1]) + l[i];
        dp[i][1] = std::max(dp[i - 2][0], dp[i - 2][1]) + h[i];
    }
    std::cout << std::max(dp[n][0], dp[n][1]) << "\n";
}             

int main(){ 
    std::ios::sync_with_stdio(false);    
    std::cin.tie(nullptr);

#ifdef LOCAL
    freopen("make.txt", "r", stdin);
    freopen("a.txt", "w", stdout);
#endif

    int T = 1;
    // std::cin >> T;
    while(T--){
        solve();
    }
    return 0; 
}   
/* 

*/  

17102 "一条路径图"的最大独立集问题

dp[i]dp[i]表示[1,i][1,i]范围内可达到的最大总权

正着递推的话,第ii个位置实际只受到前一个位置的限制

考虑将第ii个位置的权值加入总权,那么就是max(dp[i2]+w[i],w[i])max(dp[i-2]+w[i],w[i])

但是由于dp[i]dp[i]表示[1,i][1,i]范围内的最大总权,我们还需考虑不加入第ii个位置的权重时的最大总权

dp[i1]dp[i-1]

得到状态转移方程:dp[i]=max(dp[i2]+w[i],w[i],dp[i1])dp[i]=max({dp[i-2]+w[i],w[i],dp[i-1]})

  • 时间复杂度O(n)O(n)
#include<bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using pii = std::pair<int,int>;
using pll = std::pair<i64,i64>;
using pli = std::pair<i64,int>;
using pil = std::pair<int,i64>;
constexpr i64 INF = 0x3f3f3f3f3f3f3f3fll;
constexpr int inf = 0x3f3f3f3f;

void solve(){
    int n;
    std::cin >> n;
    std::vector<int>w(n + 1);
    for(int i = 1; i <= n; i++){
        std::cin >> w[i];
    }
    std::vector<i64>dp(n + 1, -INF);
    dp[0] = 0, dp[1] = std::max(w[1], 0);
    for(int i = 2; i <= n; i++){
        dp[i] = std::max(1LL * w[i], dp[i - 2] + w[i]);
        dp[i] = std::max(dp[i], dp[i - 1]);
    }
    i64 ans = *std::max_element(dp.begin(), dp.end());
    std::cout << ans << "\n";
}             

int main(){ 
    std::ios::sync_with_stdio(false);    
    std::cin.tie(nullptr);

#ifdef LOCAL
    freopen("make.txt", "r", stdin);
    freopen("a.txt", "w", stdout);
#endif

    int T = 1;
    // std::cin >> T;
    while(T--){
        solve();
    }
    return 0; 
}   
/* 

*/  

19187 广告牌最佳安放问题(二)

n,mn,m都能达到10001000,朴素的n2×mn^2 \times m的做法会超时,考虑优化掉一个nn

dp[i][j]dp[i][j]表示在第ii个位置安放第jj个广告牌的的最大收益,现在我们需要关注的是第j1j-1块广告牌安放的位置

不妨建一个长度为mm的数组preprepre[k]pre[k]用于表示距离当前位置大于五公里时,已经安放了kk块广告牌的最大收益

得到状态转移方程:dp[i][j]=pre[j1]+r[i]dp[i][j]=pre[j - 1] + r[i]

prepre数组可通过双指针法进行更新

  • 时间复杂度O(n×m)O(n \times m)
#include<bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using pii = std::pair<int,int>;
using pll = std::pair<i64,i64>;
using pli = std::pair<i64,int>;
using pil = std::pair<int,i64>;
constexpr i64 INF = 0x3f3f3f3f3f3f3f3fll;
constexpr int inf = 0x3f3f3f3f;

void solve(){
    int T, n, m;
    std::cin >> T >> n >> m;
    std::vector<int>x(n), r(n);
    for(int i = 0; i < n; i++){
        std::cin >> x[i];
    }
    for(int i = 0; i < n; i++){
        std::cin >> r[i];
    }
    std::vector<int>pre(m + 1);
    std::vector<std::vector<int>>dp(n, std::vector<int>(m + 1));
    for(int i = 0, j = 0; i < n; i++){
        while(x[i] - x[j] > 5){
            for(int k = 0; k <= m; k++){
                pre[k] = std::max(pre[k], dp[j][k]);
            }
            j++;
        }

        for(int k = 1; k <= std::min(i + 1, m); k++){
            dp[i][k] = pre[k - 1] + r[i];
        }
    }

    int ans = 0;
    for(int i = m - 1; i < n; i++){
        ans = std::max(ans, dp[i][m]);
    }
    std::cout << ans << "\n";
}             

int main(){ 
    std::ios::sync_with_stdio(false);    
    std::cin.tie(nullptr);

#ifdef LOCAL
    freopen("make.txt", "r", stdin);
    freopen("a.txt", "w", stdout);
#endif

    int T = 1;
    // std::cin >> T;
    while(T--){
        solve();
    }
    return 0; 
}   
/* 

*/