20230417 训练记录:dp

Posted Patricky

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了20230417 训练记录:dp相关的知识,希望对你有一定的参考价值。

#dp

你也许会好奇为什么没有 04.16 的训练记录,昨天农了一天 O(∩_∩)O。

Vacation

小明在接下来的 \\(n\\) 天,可以选择三种事件并获得 \\(a_i / b_i / c_i\\) 的快乐值,但是他不能连续两天及以上做同样的事。问最大的欢乐值是多少?

\\(n \\leq 10^5; a_i, b_i, c_i \\leq 10^4\\)

换句话说就是每天做的事情都不一样。用 \\(f_i, j\\) 表示第 \\(i\\) 天做 \\(j\\) 事件所得到的最大快乐值,枚举今天和明天做的事件,即:

\\[f_i + 1, j = \\max\\ f_i + 1, j, f_i, k + \\a, b, c\\_k \\, j \\ne k \\]

展开代码
#include <bits/stdc++.h>

using ll = long long;

int main() 
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
    int n;
    std::cin >> n;
    
    std::vector<std::array<int, 3>> h(n);
    for (int i = 0; i < n; i++) 
        std::cin >> h[i][0] >> h[i][1] >> h[i][2];
    
    
    std::vector<std::array<int, 3>> f(n + 1);
    
    for (int i = 0; i < n; i++) 
        for (int j = 0; j < 3; j++) 
            for (int k = 0; k < 3; k++) if (j != k) 
                f[i + 1][j] = std::max(f[i + 1][j], f[i][k] + h[i][k]);
            
        
    
    
    std::cout << *std::max_element(f.back().begin(), f.back().end()) << \'\\n\';
    
    return 0;

Knapsack 1

01 背包。

\\(n \\leq 100, w \\leq 10^5, v_i \\leq 10^9\\)

展开代码
#include <bits/stdc++.h>

using ll = long long;

int main() 
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
    int n, w;
    std::cin >> n >> w;
     
    std::vector<ll> f(w + 1);
    for (int i = 0; i < n; i++) 
        int wi, ci;
        std::cin >> wi >> ci;
        for (int j = w; j >= wi; j--) 
            f[j] = std::max(f[j], f[j - wi] + ci);
        
    
    
    std::cout << f.back() << \'\\n\';
    
    return 0;

Knapsack 2

01 背包。

\\(n \\leq 100, w \\leq 10^9, v_i \\leq 10^3\\)

考虑 \\(f_i, j\\) 为前 \\(i\\) 个物品,得到 \\(j\\) 价值的最小容量。

展开代码
#include <bits/stdc++.h>

using ll = long long;

int main() 
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
    int n, w;
    std::cin >> n >> w;
    
    const int N = n * 1000;

    std::vector<std::vector<int>> f(n + 1, std::vector<int>(N + 1, 0x3f3f3f3f));
    f[0][0] = 0;
    for (int i = 0; i < n; i++) 
        int wi, vi;
        std::cin >> wi >> vi;
        for (int j = 0; j <= N; j++) 
            f[i + 1][j] = std::min(f[i + 1][j], f[i][j]);
            if (j + vi <= N) 
                f[i + 1][j + vi] = std::min(f[i + 1][j + vi], f[i][j] + wi);
            
        
    
    
    for (int i = N; ~i; i--) 
        if (f[n][i] <= w) 
            std::cout << i << \'\\n\';
            std::exit(0);
        
    
    
    return 0;

LCS

求两字符串 \\(s, t\\) 的最长公共子序列。

Longest Path

Grid 1

Coins

Sushi

Stones

Deque

Candies

Slimes

同合并石子,区间 dp 模板题。满足四边形不等式。

展开代码
#include <bits/stdc++.h>

int read() 
    int x = 0, f = 1, c = getchar();
    for (; !isdigit(c); c = getchar()) if (c == \'-\') f = -1;
    for (; isdigit(c); x = x * 10 + c - \'0\', c = getchar());
    return x * f;


const int N = 410;
using ll = long long;

const ll inf = 1E18;
int n, a[N], s[N][N];
ll f[N][N], sum[N];

#define w(i, j) sum[j] - sum[i - 1]

int main() 
    n = read();
    for (int i = 1; i <= n; i++) 
        a[i] = read();
    

    for (int i = 1; i <= n; i++) 
        sum[i] = sum[i - 1] + a[i];
        s[i][i] = i;
    

    for (int l = 2; l <= n; l++) 
        for (int i = 1, j = l; j <= n; i++, j++) 
            f[i][j] = inf;
            for (int k = s[i][j - 1]; k <= s[i + 1][j]; k++) 
                if (f[i][j] > f[i][k] + f[k + 1][j] + w(i, j)) 
                    f[i][j] = f[i][k] + f[k + 1][j] + w(i, j);
                    s[i][j] = k;
                
            
        
    

    printf("%lld\\n", f[1][n]);

    return 0;

Matching

Indepedent Set

Flowers

Walk

Digit Sum

Permutation

Grouping

Subtree

Intervals

Tower

Grid 2

Frog 3

dp暑假专题 训练记录

A 回文串的最小划分

 题意:给出长度不超过1000的字符串,把它分割成若干个回文字串,求能分成的最少字串数。

技术分享
#include <iostream>
#include <cstdio>
#include <string.h>
#include <string>
using namespace std;
const int mod = 1e9 + 7;
const int maxn = 1000 + 5;
const int INF = 0x3f3f3f3f;
typedef long long LL;
int dp[maxn];

//根据回文串的特性,两边根据中心对称。于是我们扫描回文串的每个中心,寻找他的回文串,用DP来记录更新
//dp[n] 表示到n为止可以分割出多少个回文串
//如果[Left , Right]是回文串,
//dp[Right] = min(dp[ Right ] , dp[Left-1] + 1);

int main()
{
    int n;
    cin >> n;
    while ( n-- )
    {
        char s[maxn];
        cin >> s+1;
        int L = strlen(s+1);
        for(int i=1;i <= L;i++) dp[i] = i;

        for(int i=1;i <= L;i++)
        {
            for(int j=i,k=i; j <= L && k > 0 ;j++ ,k--)//这种是aabcc形的回文串
            {
                if(s[j] == s[k] )
                    dp[j] = min(dp[j],dp[k-1] +1);
                else
                    break;
            }
            for(int j=i+1,k=i; j <= L && k > 0 ;j++,k--)//这种是aabb形的回文串
            {
                if(s[j] == s[k] )
                    dp[j] = min(dp[j],dp[k-1] +1);
                else
                    break;
            }
        }
        cout<< dp[L]<<endl;
    }
    return 0;
}
A

 

B - LIS变形

题意:给定一串数字,求一个子串满足一下要求:子串的长度是L=2*n+1,前n+1个数字是严格的递增序列,后n+1个数字是严格的递减序列,例如123454321就是满足要求的一个子串,

输出满足要求的最长的L

思路:正着走一遍LIS,再倒着走一遍LIS,a1[i] 表示正着走的  到i的最长递增子序列的长度,a2[i] 表示倒着着走的  到i的最长递增子序列的长度

   //本题 二分 + LIS 需要巩固一下

 

技术分享
#include <iostream>
#include <cstdio>

using namespace std;
const int mod = 1e9 + 7;
const int maxn = 10000 + 5;
const int INF = 0x3f3f3f3f;
typedef long long LL;
int n,len;
int s1[maxn],s2[maxn];
int a1[maxn],a2[maxn];
int B[maxn];
int b_s(int k)
{
    int ans = 0;
    int l = 0,r = len-1; //这里注意 右区间是 len-1
    while ( l <= r)
    {
        int mid = (l+r)/2;
        if(B[mid] >= k ) ans = mid,r = mid-1; //如果k<= B[mid] 往左边查找
        else l = mid +1;
    }
    return ans ;
}

void Lis(int *s,int *a) //a数组存取到i位 的最长递增子序列个数
{
    B[0] = s[0];
    len = 1;
    for(int i=0;i < n;i++)
    {
        if(s[i] > B[len-1])
        {
            B[len++] = s[i];
        }
        else{
            int pos = b_s (s[i]); //二分找s[i]的插入位置
            B[pos] = s[i];
        }
        a[i] = len;
    }

}
int main()
{
    while (cin >> n)
    {
        for(int i=0;i < n;i++){
            cin >> s1[i];
            s2[n-1-i] = s1[i];//把序列倒着读
        }
        Lis(s1,a1), Lis(s2,a2);  //longest increasing substring 最长递增子序列函数
        int ans = 0;
        for(int i=0;i < n;i++)
        {
            ans = max(ans, min(a1[i],a2[n-1-i])); //ans 是 ans 与 当前位 左右最长递增 和 最长递减的较小者
        }
        cout<< 2*ans -1 <<endl;
    }
    return 0;
}
B

 

C - 区间dp or LCS的变形

题意:给你一个字符串, 求最长的回文子序列, 若答案不唯一, 则输出字典序最小的

思路1:区间dp, dp[i][j]表示字符串s[i]到s[j]构成回文子序列学删除的最小字符数,    path[i][j] 字符串s[i]到s[j]可构成的最长回文子序列

技术分享
#include <iostream>
#include <cstdio>
#include <string.h>
#include <string>

using namespace std;
const int mod = 1e9 + 7;
const int maxn = 1000 + 5;
const int INF = 0x3f3f3f3f;
typedef long long LL;
//区间dp dp[i][j] 表示字符串s[i]到s[j]构成回文子序列学删除的最小字符数
//path[i][j] 字符串s[i]到s[j]可构成的最长回文子序列
string path[maxn][maxn];
int dp[maxn][maxn];
char s[maxn];
int main()
{
    while (cin >> s+1)
    {
        int L = strlen(s+1);
        for(int i=L;i >= 1;i--)  //这个查找是从 左下到右上进行的
        {
            dp[i][i] = 0;//i到i不用删除
            path[i][i] = s[i];//i到i的最长回文子序列就是本身
            for(int j=i+1;j <= L;j++)
            {
                if(s[i] == s[j]) //就是 s[i][j]这两个字符相同 可以构成回文串
                {
                    dp[i][j] = dp[i+1][j-1]; //所以这时候 需要删除的就是左下角的
                    path[i][j] = s[i] + path[i+1][j-1] +s[j];//只有这一步是构造回文串的
                }
                else if(dp[i+1][j] > dp[i][j-1])
                {
                    dp[i][j] = dp[i][j-1] + 1;
                    path[i][j] = path[i][j-1];
                }
                else if(dp[i+1][j] < dp[i][j-1])
                {
                    dp[i][j] = dp[i+1][j] + 1;
                    path[i][j] = path[i+1][j];
                }
                else{
                    dp[i][j] = dp[i+1][j] + 1;
                    path[i][j] = min(path[i][j-1], path[i+1][j]);
                }
            }

           /* for(int a = 1;a<=L;a++)
            {
                for(int b= 1;b<=L;b++)
                {
                    cout<< path[a][b]<<" ";
                }
                cout<<endl;
            }
            cout<<"----------" <<endl;  */


        }
        cout << path[1][L] <<endl;
    }
    return 0;
}
C

 思路2:转化为lcs, 用结构体进行保存,一维只长度, 一维指构成的字符串 

我们都知道把一个字符串逆序后和原字符串进最长公共子序列,可以计算出它的最长回文串长度。这题不仅要输出回文串,而且还要求是字典序最小的,所以挺难搞的

设str1是正序字符串,str2是逆序后的字符串
f[i][j].len 表示str1的前i位,str2的前j位,最长公共子串的长度
f[i][j].str 表示str1的前i位,str2的前j位,最长公共子串的最小字典序的字符串

状态转移和正常的LCS差不多,只不过增加了记录字典序最小的字符串

但是最终的f[i][j].str却并不一定是答案,因为计算出来的最长公共子序列不一定就是回文串

例如:
kfclbckibbibjccbej
jebccjbibbikcblcfk

bcibbibc是他们的LCS,但是却不是回文串

但是它的前len/2个一定是回文串的前半部分

技术分享
#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;
using namespace std;
const int mod = 1e9 + 7;
const int maxn = 1000 + 5;
const int INF = 0x3f3f3f3f;
typedef long long LL;

struct node{
    int len;
    string s;
}dp[maxn][maxn];
char s1[maxn],s2[maxn];

int main ()
{
    while (cin >> s1+1)
    {
        int L = strlen(s1+1);
        strcpy(s2+1,s1+1);
        reverse(s2+1,s2+1+L);

        for(int i=1;i <= L;i++)
        {
            for(int j=1;j <= L;j++)
            {
                if(s1[i] == s2[j]){
                    dp[i][j].len = dp[i-1][j-1].len+1;
                    dp[i][j].s = dp[i-1][j-1].s + s1[i];
                }
                else if(dp[i-1][j].len < dp[i][j-1].len){
                    dp[i][j].len = dp[i][j-1].len;
                    dp[i][j].s = dp[i][j-1].s;
                }
                else if(dp[i-1][j].len > dp[i][j-1].len){
                    dp[i][j].len = dp[i-1][j].len;
                    dp[i][j].s = dp[i-1][j].s;
                }
                else{
                    dp[i][j].len = dp[i-1][j].len;
                    dp[i][j].s = min( dp[i-1][j].s ,dp[i][j-1].s);
                }
            }
        }
        string s = dp[L][L].s;
        int l =dp[L][L].len;
        if( l & 1) //l是奇数,所以字符串长度是偶数
        {
            for(int i=0;i<l/2;i++)
                cout<<s[i];
            for(int i=l/2;i>=0;i--)
                cout<<s[i];
            cout<<endl;
        }
        else{
            for(int i=0;i<l/2;i++)
                cout<<s[i];
            for(int i=l/2-1;i>=0;i--)
                cout<<s[i];
            cout<<endl;
        }

    }
    return 0;
}
C_LCS

 

以上是关于20230417 训练记录:dp的主要内容,如果未能解决你的问题,请参考以下文章

dp暑假专题 训练记录

Dailight 训练记录

ECNA 2014 部分题解 | 训练记录0703

ECNA 2014 部分题解 | 训练记录0703

斜率优化系列——训练记录

2017个人训练记录