动态规划

动态规划

P2758 编辑距离

我们用 发 \(f[i][j]\) 表示将 \(A\) 串的前 \(i\) 个字符变为 \(B\) 串的前 \(j\) 个字符所需的最小操作数,接下来我们对三种操作进行分析来得到转移方程。

首先是添加一个字符,我们可以将其看做将 \(A\) 串的第 \(i-1\) 个字符与 \(B\) 串的第 \(j\) 个字符匹配,即将 \(A\) 串的第 \(i-1\) 个字符变为 \(B\) 串的第 \(j\) 个字符。如下图,此时 \(i=4\) ,我们将原来的 \(A[4]=q\) 前添加一个字符 \(g\) , \(q\) 后面的字符都后移一位,相当于 \(i\) 继续与 \(j-1\) 匹配。转移方程便为 \(f[i][j]=\min(f[i][j],f[i][j-1]+1)\).

sfdqxbw
gfdgw
sfdgqxbw
12345678

然后是添加一个字符,我们可以将其看做将 \(A\) 串的第 \(i\) 个字符与 \(B\) 串的第 \(j-1\) 个字符匹配,即将将 \(A\) 串的第 \(i\) 个字符变为 \(B\) 串的第 \(j-1\) 个字符。如下图,此时 \(i=3\) ,我们将原来的 \(A[4]=q\) 前删除字符 \(q\) , \(q\) 后面的字符都前移一位,相当于 \(i-1\) 继续与 \(j\) 匹配。转移方程便为 \(f[i][j]=\min(f[i][j],f[i-1][j]+1)\).

sfdqxbw
gfdgw
sfdxbw
12345678

接下唉便是最简单的修改了,只要当前 \(A[i]!=B[j]\) 我们就可以考虑删除,即 \(f[i][j]=f[i-1][j-1]+1\) ,当然修改的前提是\(A\) 串的第 \(i\) 个字符与 \(B\) 串的第 \(j-1\) 个字符不同,否则我们正常转移,即 \(f[i][j]=f[i-1][j-1]\)

最后便是初始状态将 \(A\) 串的前 \(i\) 个字符变为空的 \(B\) 串的前 \(j\) 个字符需要 \(i\) 次添加,所以 \(f[i][0]=i\) ,同理可得 \(f[0][j]=j\) .

#include<bits/stdc++.h>
using namespace std;
string s1,s2;
int f[2048][2048];
int main(){
   cin>>s1>>s2;
   int l1=s1.size(),l2=s2.size();
   for(int i=1;i<=l1;i++){
    f[i][0]=i;
   }
   for(int i=1;i<=l2;i++){
    f[0][i]=i;
   }
   for(int i=1;i<=l1;i++){
    for(int j=1;j<=l2;j++){
        if(s1[i-1]!=s2[j-1]){
            f[i][j]=f[i-1][j-1]+1;
        }else{
            f[i][j]=f[i-1][j-1];
        }
        f[i][j]=min(f[i][j],f[i-1][j]+1);
        f[i][j]=min(f[i][j],f[i][j-1]+1);
    }
   }
   cout<<f[l1][l2];
    return 0;
}

[P1435 IOI 2000] 回文字串

以下所写的字符串下标都是从 \(1\) 开始。

假设一个字符串 \(S\) ,它的长度为 \(n\) .如果它是一个回文字符串,那么对于它的子串 \(S_{1+i \; n-i}\) 也一定是回文的。所以我们可以从这点下手,对于一个字符串 \(A\) 如果它的子串 \(A_{i+1 \; j}\) 是回文串,我们可以将添加一个 \(A_i\) 放在后面使它变成回文串;如果 \(A_{i \; j-1}\) 是回文串,我们可以添加一个 \(A_j\) 放在前面使它变成回文串;如果 \(A_{i+1 \; j-1}\) 是回文串,只要 \(A_i=A_j\) 那么 \(A\) 本身便满足回文串的性质,我们便不用添加字符使它变成回文串。

由此我们可以设 \(f[i][j]\) 表示要把从 \(i\)\(j\)(包括 \(i\)\(j\) )的字符串变为回文串的最少插入字符数。根据上面的推理我们很容易得到转移方程 \(f[i][j]=\min(f[i+1][j-1](A_i==A_j),f[i+1][j]+1,f[i][j-1]+1)\) .初始状态分为两种,一是长度为 \(0\) 的子串,即只有一个字符,它一定是回文串,即 \(f[i][i]=0 \quad i\in [1,A.size]\) .对于长度为 \(1\) 的子串,如果它的两个字符相同就是回文串(\(f[i][j]=0\)),否则就不是回文串(\(f[i][j]=1\))。末态便是我们要将长度为 \(1——A.size\) 的字符串变成回文串,即 \(f[1][A.size]\) .

#include<bits/stdc++.h>
using namespace std;
string s1,s2;
int f[1024][1024];
int main(){
    cin>>s1;
    int len=s1.size();
    s1=' '+s1;//下标从1开始,方便dp
    for(int i=1;i<=len;i++){//初始化
        f[i][i]=0;
    }
    for(int i=2;i<=len;i++){
        if(s1[i-1]==s1[i]){
            f[i-1][i]=0;
        }else{
            f[i-1][i]=1;
        }
    }
    for(int l=2;l<len;l++){//枚举子串的长度
        for(int i=1;i+l<=len;i++){
            int j=i+l;
            if(s1[i]==s1[j]){
                f[i][j]=f[i+1][j-1];
            }else{
                f[i][j]=1e9;
            }
            f[i][j]=min(f[i][j],min(f[i+1][j],f[i][j-1])+1);
        }
    }
    cout<<f[1][len];
    return 0;
}

P1854 花店橱窗布置

我们定义 \(f[i][j]\) 为前 \(i\) 朵花在前 \(j\) 个花瓶的最大美学值,那么我们就可以分为两种情况

1.第 \(i\) 朵花放入了第 \(j\) 个花瓶,那么 \(f[i][j]=f[i-1][j-1]+a[i][j]\)

2.第 \(i\) 朵花没有放入第 \(j\) 个花瓶,那么 \(f[i][j]=f[i][j-1]\)

初始状态为没有花时,无论放在哪个花瓶中美学值都为 \(0\)\(f[0][j]=0 \; j\in[0,m]\).目标状态便是 \(f[n][m]\)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5;
const int mod=1e9+7;
int f[128][128],a[128][128],n,m,path[128],cnt;
void print(int n,int m){//递归输出
	if(!n||!m){
        return ;
    }
    while(f[n][m]==f[n][m-1]){
        m--;
    }
    print(n-1,m-1);
    cout<<m<<" ";
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            cin>>a[i][j];
        }
    }
    memset(f,-0x3f,sizeof(f));//初始赋成负无穷
    for(int i=0;i<=m;i++){//初始状态
        f[0][i]=0;
    }
    for(int i=1;i<=n;i++){
        for(int j=i;j<=m;j++){
            f[i][j]=max(f[i][j-1],f[i-1][j-1]+a[i][j]);
        }
    }
    cout<<f[n][m]<<"\n";
    print(n,m);
    return 0;
}

P1725 琪露诺

这道题我们很容易想到一个 \(f[i]\) 便是琪露诺在 \(i\) 位置上所能达到的最大冰冻值。那么也很容易想到一个转移方程

\[f[i]= \max(f[i],f[j]+a[i]); \; j\in[i-r,i-l] \]

初始状态便是 \(f[k]=0 \; k\in[0,l-1] \; 0到l-1的格子跳不到\) 终态便是 \(\max(f[k]) \; k\in[n-r+1,n]\) 要跳到 \(>n\) 的位置。

但这样时间复杂度达到 \(O(n^2)\) 拿不了满分(拿不到 \(subtast\) 的分)

所以我们就考虑时间优化。

我们可以发现\(f[i]\)的取值只能由决策区间\([i-r,i-l]\)转移而来,而\(a[i]\)是定值,所以我们只用考虑\(f[i-r].... f[i-l]\)的最优选择.也就是 \([i-r,i-l]\) 这段区间中 \(f\) 最大值。而像这样的区间相互重叠有多个,所以我们可以用单调队列优化,每次取当前区间的最大值,在像滑动窗口那样移动区间。

#include<bits/stdc++.h>
using namespace std;
const int N=1e6;
int n,l,r,hh,ta,ans;
int a[N],f[N],q[N];
int main(){
    cin>>n>>l>>r;
    ans=-1e9;
    memset(f,-0x3f,sizeof(f));//因为有负数,要初始化正无穷
    for(int i=0;i<=n;i++){
        cin>>a[i];
    }
    f[0]=0;
    for(int i=l;i<=n;i++){//前面的l的格子跳不到
        while(hh<ta&&f[i-l]>=f[q[ta-1]]){
            ta--;
        }
        q[ta++]=i-l;//单调队列里存档的是下标
        while(hh<ta&&q[hh]<i-r){//队首过期出队
            hh++;
        }
        f[i]=f[q[hh]]+a[i];
        if(i+r>n){//能跳到最后的位置就记录
            ans=max(ans,f[i]);
        }
    }
    cout<<ans;
    return 0;
}

[P2679 NOIP 2015 提高组] 子串

我们可以直接那题意作为状态,用 \(f[i][j][k]\) 表示 \(A\) 串的第 \(i\) 位匹配到了 \(B\) 串第 \(j\) 位划分了 \(k\) 个子串的方案数、

当·\(A_i=B_j\) 时有两种情况,首先是我们还是在这个字串的后面,这时子串的数量没变,那么 \(f[i][j][k]\) 就可以由 \(f[i-1][j-1][k]\) 转移过来,但是接下来的情况有些复杂,那就是我们此时与与 \(B\) 串第 \(j\) 位匹配的是新的子串,\(j\) 依旧由 \(j-1\) 变过来,而 \(k\) 也是有 \(k-1\) 变过来,那么我们的 \(i\) 就不确定了,因为它可以由前面任何一个 \(i\) 的位置转移过来,可以是 \(i-1 \; i-2 ...\)

A:aabaab 
B:aab
  123456

例如上面的字符串,要取出 \(3\) 个子串与 \(B\) 匹配(即 \(k=2\)\(i=5\) 时匹配 \(B[j=3]\) ,那么匹配 \(B[j=2]\) 可以是 \(i=2 \quad i=4 \quad i=5\) 。所以我们就需要一个辅助数组来帮助我们。

我们定义 \(g[i][j][k]\) 表示 \(A\)\(i\) 位匹配字符串 \(B\) 串的第 \(j\) 位划分 \(k\) 个子串的方案数。\(g[i][j]\) 其实相当于 \(f[i][j]\) 的前缀和(当然 \(k,j\) 要相等)。\(g[i][j][k]=\sum_{x=1}^{i}f[x][j][k]\) .

最终我们得到转移方程 \(f[i][j][k]=f[i-1][j-1][k]+g[i-1][j-1][k-1]\)

\(g[i][j][k]=g[i-1][j][k]+f[i][j][k]\)

因为 \(O(nmk)\) 的空间会炸,所以我们要使用滚动数组。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e6;
const int mod=1e9+7;
ll f[1024][256][2],g[1024][256][2],n,m,k;
string a,b;
int main(){
    cin>>n>>m>>k>>a>>b;
    a=' '+a;b=' '+b;
    for(int i=0;i<=n;i++){
        g[i][0][0]=1;
    }
    int line=1;
    for(int kk=1;kk<=k;kk++){
        for(int i=0;i<=n;i++){
            for(int j=0;j<=m;j++){
                f[i][j][line]=0;
                g[i][j][line]=0;
            }
        }
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                if(a[i]==b[j]){
                    f[i][j][line]=(f[i-1][j-1][line]+g[i-1][j-1][line^1])%mod;    
                }
                g[i][j][line]=(g[i-1][j][line]+f[i][j][line])%mod;
            }
        }
        line^=1;
    }
    cout<<g[n][m][line^1];
    return 0;
}

P4310 绝世好题

首先有个很好想的思路就是定义 \(f[i]\) 表示以 \(a[i]\) 结尾的子序列的最长长度,初始状态就是 \(f[i]=1\) ,转移方程是 \(f[i]=\max(f[i],f[j]+1)(a[i]\&a[j]!=0)\quad j\in[1,i]\) ,最终状态便是 \(\max(f[i]) \; i\in[1,n]\) .这样的时间复杂度是 \(O(n^2)\) ,会超时,导致只有九十分。

接下来便是优化思路,我们可以从位运算的角度考虑优化。我们需要找到一个子序列,其中相邻元素的按位与不为零。这意味着相邻元素至少有一个共同的二进制位为1。因此,我们不需要关心具体是哪个数,只需要关心每个二进制位上的最长序列。重新定义 \(f[i]\) 表示数列到目前为止最后一项第 \(i\) 位为1的最大子序列长度,我们将 \(a[i]\) 的每一位拆开,如果这一位是 \(1\) ,我们记录以 \(a[i]\) 结尾最长子序列的长度,也就是在所有满足条件的 \(f[j]\) 中取最大。同时,对于满足条件的 \(f[j]\),它可以被这个最大值转移过来。即 \(f[j]=\max(f[j]+1)(a[i]的第j位是1)\)

#include<bits/stdc++.h>
using namespace std;
const int N=1e6;
int f[64],n,a[N],ans;
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    for(int i=1;i<=n;i++){
        int maxn=0;
        for(int j=0;j<=31;j++){
            if(a[i]>>j&1){//取可以转移的最大值
                maxn=max(maxn,f[j]+1);
            }
        }
        for(int j=0;j<=31;j++){
            if(a[i]>>j&1){//将最大值在转移过去
                f[j]=maxn;
            }
        }        
        ans=max(ans,maxn);
    }
    cout<<ans;
    return 0;
}

P1775 石子合并(弱化版)

\(f[i][j]\) 表示区间 \([i,j]\) 内合并的最小代价。而区间 \([i,j]\) 可以有任意区间 \([i,i+k]\)\([k+1,j]\) 合并而来,也就是说,我们设置一个断点 \(k\) ,将这个区间断成两部分,在移动这个断点,寻找这个合并的最小代价。由此可以得到转移方程 $$f[i][j]=\min{f[i][k]+f[k+1][j]+v}(i\le k\lt j)$$ ,而这个 \(v\) 便是两个区间合并所需的代价,\(v=\sum\limits_{x=i}^{j}a_x\) .这样看我们枚举左右区间,断点和价值的时间复杂度是 \(O(N^4)\) 。我们可采取前缀和优化,提前计算出 \(v\) ,那么时间复杂度便可优化为 \(O(N^3)\)

初始状态便是区间长度为 \(0\) 时合并的代价就是\(0\),即 \(f[i][i]=0\) 。最终答案为合并整个区间的代价 \(f[1][n]\) .

#include<bits/stdc++.h>
using namespace std;
int f[512][512],a[512],sum[512],n;
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        sum[i]=sum[i-1]+a[i];//记录前缀和
    }
    memset(f,0x3f,sizeof(f));
    for(int i=1;i<=n;i++){
        f[i][i]=0;
    }
    for(int l=1;l<n;l++){
        for(int i=1;i+l<=n;i++){
            int j=i+l;
            for(int k=i;k<j;k++){
                f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+sum[j]-sum[i-1]);
            }
        }
    }
    cout<<f[1][n];
    return 0;
}

[P1880 NOI1995] 石子合并

这道题和上一道题基本一样,最大的区别就是从原来的一条链变成了环形摆放的石子。我们这里就要用一个小技巧破环为链,也就是将环上的操作转化链上。我们可以将原来的石子序列复制一次接在后面,能得到一条长度为 \(2n\) 的序列,我们可以按照上一题的思路对这条序列进行拆分,再寻找最优解。但是我们最终需要在这条序列中找到所以长度为 \(n\) 的子序列合并的最大值和最小值。

初始状态:\(f[i][i]=0\) \(g[i][i]=0\)

转移方程:$$f[i][j]=\min{f[i][k]+f[k+1][j]+v}\quad g[i][j]=\max{g[i][k]+g[k+1][j]+v}(i\le k\lt j)(j-i\lt n)$$ \(v=\sum\limits_{x=i}^{j}a_x\)

最终状态:\(\min(f[i][i+n-1]) \quad max(g[i][i+n-1]) \quad (i\in[1,n])\)

#include<bits/stdc++.h>
using namespace std; 
int f[512][512],a[512],sum[512],n,g[512][512];
//f求最小值 g求最大值
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        sum[i]=sum[i-1]+a[i];
    }
    for(int i=n+1;i<=n*2;i++){//将原序列复制一次
        a[i]=a[i-n];
        sum[i]=sum[i-1]+a[i];
    }
    memset(f,0x3f,sizeof(f));
    memset(g,-0x3f,sizeof(g));
    int m=n*2;//m表示加长序列的长度
    for(int i=1;i<=m;i++){
        f[i][i]=0;
        g[i][i]=0;
    }
    for(int l=1;l<n;l++){
        for(int i=1;i+l<=m;i++){
            int j=i+l;
            for(int k=i;k<j;k++){
                f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+sum[j]-sum[i-1]);
                g[i][j]=max(g[i][j],g[i][k]+g[k+1][j]+sum[j]-sum[i-1]);
            }
        }
    }
    int minn=1e9,maxn=-1e9;
    for(int i=1;i<=n;i++){//寻找所有长度为n的序列最优解
        minn=min(minn,f[i][i+n-1]);
        maxn=max(maxn,g[i][i+n-1]);
    }
    cout<<minn<<"\n"<<maxn;
    return 0;
}

[P1063 NOIP 2006 提高组] 能量项链

这道题也是和石子合并一样破环成链,也是一样将原序列复制成 \(2n\) ,也是一样寻找长度为 \(n\) 的区间。我们设 \(f[i][j]\) 表示合并区间 \([i,j]\) 的最大总能量,初始状态便是 \(f[i][i]=0\) ,转移方程也很好想,对于一个珠子它有头和尾两个属性,第 \(i\) 颗珠子的头便是 \(a[i]\) ,尾是 \(a[i+1]\) (除了最后一个珠子),我们设第 \(i\) 颗珠子的为 \(num[i]\) ,它的头和尾非标是 \(h\)\(t\) .那么转移方程便是 $ f[i][j]=\max{f[i][k]+f[k+1][j]+num[i].h\times num[k].t \times num[j].t}(i\le k\lt j)$ .最终答案就是 \(max(f[i][i+n-1]) \quad (i\in[1,n])\)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll n,a[256],f[1024][1024];
pair<ll,ll> num[256];
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    } 
    int m=n*2;
    for(int i=n+1;i<=m;i++){
        a[i]=a[i-n];
    }
    for(int i=1;i<m;i++){
        num[i].first=a[i];
        num[i].second=a[i+1];
    }
    num[m].first=a[m],num[m].second=a[1];
    for(int l=1;l<n;l++){
        for(int i=1;i+l<m;i++){
            int j=i+l;
            for(int k=i;k<j;k++){
                f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+num[i].first*num[k].second*num[j].second);
            }
        }
    }
    ll maxn=-1;
    for(int i=1;i<=n;i++){
        maxn=max(f[i][n+i-1],maxn);
    }
    cout<<maxn;
    return 0;
}

[P4170 CQOI2007] 涂色

定义 \(f[i][j]\) 表示将区间 \(i\)\(j\) 染成目标状态所需要的最少次数。初始状态为 \(f[i][i]=1\) ,那么我们涂色的方式可以概括为两周

1.覆盖,先涂一个大区间,然后再涂一个小区间使其包含在大区间内。

`AAABBBAAA

上面例子中可以先将整个区间涂成A,再在中间部分连续涂三个B

2.单独涂色,涂两个并列且没有交集的区间。(如果有交集会互相覆盖,又相当于没有交集了,所以不用考虑)

AAABBBAAA

先单独涂3个A,再单独涂3个B,最后涂3个A

那么我们转移也有两种方法,首先是当 \(a[i]=a[j]\) 时,\(f[i][j]=\min(f[i+1][j],f[i][j-1])\) 我们可以将其看做最开始涂大区间是多向左或右涂了一格。

其次是无论如何我们都可以使用第二种方法,我们可以枚举断点,将小区间合并为大区间,即\(f[i][j]=\min( f[i][j],f[i][k]+f[k+1][j])\).

目标状态便是将 \(f[1][n]\)

#include<bits/stdc++.h>
using namespace std;
int f[256][256],n;
string s1;
int main(){
    cin>>s1;
    n=s1.size();
    s1=' '+s1;
    memset(f,0x3f,sizeof(f));
    for(int i=1;i<=n;i++){
        f[i][i]=1;
    }
    for(int l=1;l<n;l++){
        for(int i=1;i+l<=n;i++){
            int j=i+l;
            if(s1[i]==s1[j]){
                f[i][j]=min(f[i][j-1],f[i+1][j]);
            }
            for(int k=i;k<j;k++){
                f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]);
            }
        }
    }
    cout<<f[1][n];
    return 0;
}

写这个题真是累啊,好毒。应该练练处理比较麻烦的DP。

在读完题目以后,其实初始版的方程并不难想

\(dp[i][j][k]\)表示时间\(i\)在工厂\(j\)当机器人前已经走了\(k\)步的方案。

\(k\)那一维可以通过直接枚举消去

\(dp[i][j][1/0]\)表示时间\(i\)工厂\(j\)是否能够继续再走

转移为
\(dp[i][j][1]=max(dp[i][j][1],dp[i-k][j-k][0]+cal(i,j,k))\)//没有列出关于环的情况的,以下会详细说
\(dp[i][j][0]=max(dp[i][j][0],dp[i][k][1]-cost[j])\)

确实很不完美,我当时也只是想到了这么多,感觉过个90还是比较轻松的,1000的点拿单调队列优化一下就行了。

然后我就开始写\(cal\)函数,处理那个对角线式的前缀和,然后成功把自己搅糊了。

这个题把点权释放到了边权上,人话就是存储的路径位置其实是某个点伸出去的那一条

比如用这个图来描述读入的某时间某费用数组

\(f[i][j]\)表示时间\(i\)位置\(j\)所延伸回去的45°的链的值,如下图,黄点为\((i,j)\)的位置,则它所代表的链为蓝色的一条链上的点权之和。

好吧,弄清楚了这个,写一下\(cal(i,j,k)\)函数了,如下图,它要返回这样一个链的值

事实上看起来也不是那么麻烦

int cal(int i,int j,int k)
{
    return f[i-1][j-1]-f[i-k-1][j-k-1];
}

但是,当转移设计到拐弯时

确实有点麻烦。。。我最开始漏掉了那条虚线。。

这是带拐弯的转移:\(dp[i][j][1]=max(dp[i][j][1],dp[i-k][n+j-k][0]+cal(i-j+1,n+1,k-j+1)+cal(i,j,j-1))\)

好吧,到这里我已经感觉我写不出单调队列了。

交了一下果然拿到了90分,其实在如果在考场上做到这里已经可以了(鬼知道为什么部分分有这么多)

部分分代码:

#include <cstdio>
#include <cstring>
int max(int x,int y){return x>y?x:y;}
int min(int x,int y){return x<y?x:y;}
const int N=1010;
const int inf=0x3f3f3f3f;
int dp[N][N][2],n,m,p,harv[N][N],f[N][N],cost[N],ans=-inf;//n数量,m时间
int cal(int i,int j,int k)
{
    return f[i-1][j-1]-f[i-k-1][j-k-1];
}
int main()
{
    memset(dp,-0x3f,sizeof(dp));
    scanf("%d%d%d",&n,&m,&p);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
        {
            scanf("%d",&harv[i][j]);
            f[j][i]=f[j-1][i-1]+harv[i][j];
        }
    for(int i=1;i<=n;i++)
        scanf("%d",cost+i);
    for(int i=1;i<=n;i++)
        dp[1][i][0]=-cost[i];
    m++;
    for(int i=2;i<=m;i++)//时间
    {
        for(int j=1;j<=n;j++)//路程
        {
            for(int k=1;k<=min(i,p);k++)//从第几个之前转移
            {
                if(j>k)
                    dp[i][j][1]=max(dp[i][j][1],dp[i-k][j-k][0]+cal(i,j,k));
                else if(i>j)
                    dp[i][j][1]=max(dp[i][j][1],dp[i-k][n+j-k][0]+cal(i-j+1,n+1,k-j+1)+cal(i,j,j-1));
            }

        }
        for(int j=1;j<=n;j++)
            for(int k=1;k<=n;k++)
            {
                if(k==j) continue;
                dp[i][j][0]=max(dp[i][j][0],dp[i][k][1]-cost[j]);
            }
    }
    for(int i=1;i<=n;i++)
        ans=max(ans,dp[m][i][1]);
    printf("%d\n",ans);
    return 0;
}

看看各位佬爷的题解。


才发现自己的方程太不优秀了,优秀的方程\(O(N^3)\)甚至可以卡过。

\(dp[i]\)表示时间\(i\)的最大答案。

转移:\(dp[i]=max(dp[i-k]+cal(i,j,k)-cost[j-k])\)//无环

看着减去的\(cost[i]\),我明白了应该给\(dp[i]\)加一个定语

\(dp[i]\)表示时间\(i\)处在某位置上还未在此位置上消费机器人的最大答案,每一步的机器人花费是在被转移的时候才扣得啊。而我最初的方程,是代表当前时间\(i\)和地点\(j\)已经买了机器人的最大答案。为了区分是否处理花费机器人的状态,我甚至得用第三维的0/1维护。

读了读题目中“必须立刻在 任意 一个机器人工厂中购买一个新的机器人”,我明白了为什么可以不要地点这一维,其实每一个时间都可以当做是步数已经到了的时间,而此时地点的选择是具有任意性的,即此时地点也是不重要的。

我把方程改了改,果然\(O(N^3)\)卡过了


单调队列优化

但是,如果数据再卡一点,单队优化就是必须的了。

在这里,因为实在是觉得这种点权下放的方式不优雅,我将工厂的实际编号和时间给减去了1,而读入时不变

这样,查询\(f[i][j]\)所代表的就不是再多延伸出去一条链了,很舒服了。

再列出转移方程://无环
\(dp[i]=max(dp[i-k]+f[i][j]-f[i-k][j-k]-cost[j-k])\)
\(=max(dp[i-k]-f[i-k][j-k]-cost[j-k])+f[i][j]\)

转移时维护\(q[i][j]=dp[i]-f[i][j]-cost[j]\)即可

因为每一个\(f[i][j]\)都可以唯一的确定一个\(dp[i]\)\(cost[j]\),所以我们考虑\(f[i][j]\)在转移时的分布。

对同一个答案的贡献,这个分布大概是这样。

为了准确的定位某个答案从哪个单调队列转移,考虑给每一个单调队列编号,将单队与\(location\)轴相交的那个点作为它的编号。

在还没拐弯时,所属单队即为\(j-i\),拐弯了以后我们发现它减去了\(l\)\(n\)\(l\)为拐弯次数,很简单,取膜以后加一个再取膜即可

int get(int i,int j)//获取单队编号
{
    return ((j-i)%n+n)%n;
}

还有两点要注意的地方

一是虚线所连的边仍然需要特判一下
二是为了确保拐弯后不出现问题,要把\(dp[i]=max(dp[i-k]-f[i-k][j-k]-cost[j-k])+f[i][j]\)中的\(f[i][j]\)加上它失去的链的长度,维护一个\(add[i]\)数组。

参考代码:

#include <cstdio>
#include <cstring>
const int N=1010;
int max(int x,int y){return x>y?x:y;}
int n,m,p;
int f[N][N],cost[N],q[N][N],loc[N][N],l[N],r[N],add[N],dp[N];
int get(int i,int j)//获取单队编号
{
    return ((j-i)%n+n)%n;
}
int main()
{
    scanf("%d%d%d",&n,&m,&p);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
        {
            scanf("%d",&f[j][i]);
            f[j][i]+=f[j-1][i-1];
        }
    for(int i=0;i<n;i++)
    {
        scanf("%d",cost+i);
        q[i][++r[i]]=-cost[i],l[i]++;
    }
    memset(dp,-0x3f,sizeof(dp));
    dp[0]=0;
    for(int i=1;i<=m;i++)
    {
        for(int j=0;j<n;j++)
        {
            int id=get(i,j);
            while(l[id]<=r[id]&&loc[id][l[id]]+p<i) l[id]++;
            if(!j) add[id]+=f[i][n];
            if(l[id]<=r[id])
                dp[i]=max(dp[i],q[id][l[id]]+add[id]+f[i][j]);
        }
        for(int j=0;j<n;j++)
        {
            int id=get(i,j);
            int tmp=dp[i]-add[id]-f[i][j]-cost[j];
            while(l[id]<=r[id]&&q[id][r[id]]<=tmp)
                r[id]--;
            loc[id][++r[id]]=i;
            q[id][r[id]]=tmp;
        }
    }
    printf("%d\n",dp[m]);
    return 0;
}

posted @ 2025-06-08 18:22  金牌白鸽  阅读(25)  评论(0)    收藏  举报