题解:P13501 「Cfz Round 6」Imaichi

· · 题解

::::info[真的有人会关心题目名称吗?]

题目背景是日语歌词,所以猜测题目名称是日语单词,尝试用平假名拼写,得到了いまいち。

いまいち (imaichi)

词性: 副词 / 形容动词词干 (口语化、较随意)

含义: 来源于“今一つ (いまひとつ)”,意思是“还差一点”、“不够好”、“不太满意”、“差强人意”。形容感觉上稍有欠缺,未能达到理想状态。

摘自 DeepSeek。

虽然我不能得知这题目名称背后的含义,其中的含义又岂可与人语,大概只有经历过才能知道吧。但是如果这篇题解有幸能被 Cfz 看到,能否告诉我我的猜测是否正确?

::::

虽然我没能通过这道题,但是我认为我的做法时间复杂度是正确的,仍然具有参考价值。

赛后我在 DeepSeek 的帮助下获得了 AC,但是写出了 7.33KB 史山,反而不具有参考价值。以下题解只介绍原来的做法,由于常数过大与水平有限卡常失败,即使时间复杂度是正确的也会 TLE 70pts,但我认为这样的思路仍然具有参考价值

题目描述

Yuki 需要在 nm 列的矩阵上从第一行的任意一个格子出发走到最后一行的任意一个格子停止,走进最后一行后立即结束。初始时 Yuki 有 s 摩拉,每当走进第 i 行第 j 列的格子时就会获得 a_{i,j} 摩拉,但是最多不能同时拥有超过 k 摩拉。如果走到某个格子后摩拉数量为负数就会被拘留,不能走到终点了。问走到最后一行的任意一个格子时剩余的摩拉数量的最大值。

思路

每一步只能向左边、右边、或者下边走。所以可以在一行里反复移动。

如果 a_{i,j}+a_{i,j+1}>0 ,我们称这是一个刷钱点。显然每个刷钱点至少有一个正数。所以如果走到了正数的位置,那么可以把钱刷满。

所以说当我们走到某个格子的时候,不必着急向下走,可以到这一行的刷钱点把摩拉刷到上限。但是即使在刷钱点刷到 k,也会在回去的时候被减少。所以选择左边或者右边最近的刷钱点一定不劣。

我们称在 (i,j) 出存在一个刷钱点,当且仅当 a_{i,j-1}+a_{i,j}>0a_{i,j}+a_{i,j+1}>0 满足其中任意一个并且满足 a_{i,j} \geq 0

p_j 为如果从 (i,last) 出发时有 k 摩拉,走到 (i,j) 后还剩多少摩拉;q_j 为如果从 (i,j) 出发,抵达 (i,last) 最少需要在开始时有多少个摩拉。

可以发现,pq 是可以递推的。具体来说,p_j=\text{max}(p_{j-1}-a_{i,j},0)q_j=\min(q_{j-1}+a_{i,j},k)。从右往左递推同理。当从左往右递推时,如果 (i,j) 上存在刷钱点,那么 p_j=k,q_j=0,last=j

所以,设 e_{i,j} 为进入 (i,j) 时剩余的最多摩拉数量,如果 e_{i,j} \geq p_{j-1},则可以从 (i,j) 出发向左移动到刷钱点然后返回,最后返回到 (i,j) 时最多还剩 q_j 个摩拉。向右移动同理。

但是,我们还可以在某个地方得到在这个地方可以获得的最多的摩拉之后不再返回原点,而是走向其他地方。显然当在一个地方获得最多的摩拉之后向一个方向移动一定不劣。所以可以从左往右递推然后从右往左递推。具体来说,在从左往右递推时可以记录走到某一点时剩余最大的摩拉数量,如果现在最大的摩拉数量超过原来的就更新最大数量。从右往左同理。

但是,需要考虑并不是每一个 (i,j) 都可以从 (i-1,j) 进入。需要在开始的时候再从左到右、从右到左筛两边判断哪些点可以作为出发点。

总时间复杂度为 O(nmT),但是因为我的水平有限被卡常。

最后是乱七八糟的代码。但是因为常数太大水平有限不会卡常 TLE 70pts,但是就算不够完美也好。

#include <bits/stdc++.h>
//...
//真的对吗?
using namespace std;
int n,m,s,k; 
int a[1023][1023];
int e[1023][1023],dp[1023][1023];//进入时在结算后最多还剩多少钱,将要退出时最多还剩多少钱 
constexpr int inf=0x3f3f3f3f;
int p[1023],q[1023];//去的时候至少要多少钱和回来的时候最多还剩多少钱 
//p是说在结算这个格子之前至少要有多少钱 
bool flag1[1023],flag2[1023],flag[1023];//从可以开始的点出发,有哪些点是可以到达的 
int mian()
{
    cin>>n>>m>>s>>k;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            cin>>a[i][j];
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            e[i][j]=dp[i][j]=-inf;
    for(int i=0;i<=m+1;i++)
        p[i]=inf,q[i]=-inf;
    p[0]=0,p[m+1]=0;
    for(int i=1;i<=m;i++)
    {
        int p=k<s+a[1][i]?k:s+a[1][i];
        if(p>=0)
            e[1][i]=p;
    }
    for(int i=1;i<n;i++)
    {
        for(int j=1;j<=m;j++)
            dp[i][j]=e[i][j];
        memset(flag1,0,sizeof(flag1));
        memset(flag2,0,sizeof(flag2));
        for(int j=m;j>=1;j--)
        {
            if(e[i][j]>=0)
            {
                int cur=j,remain=e[i][j];
                while(cur<=n&&remain>=0)
                {
                    flag1[cur]=true;
                    if(cur!=j)
                        remain=min(remain+a[i][cur],k);
                    cur++;
                }
            }
        }
        for(int j=1;j<=m;j++)
        {
            if(e[i][j]>=0)
            {
                int cur=j,remain=e[i][j];
                while(cur>=1&&remain>=0)
                {
                    flag2[cur]=true;
                    if(cur!=j)
                        remain=min(remain+a[i][cur],k);
                    cur--;
                }
            }
        }
        for(int j=1;j<=m;j++)
            flag[j]=flag1[j]|flag2[j];
        //考虑向左走找到刷钱点
        int last=-inf;
        for(int j=1;j<=m;j++)
            p[j]=inf,q[j]=-inf;
        for(int j=1;j<=m;j++)
        {
            //判断这是不是刷钱点
            //刷钱点是两个正数或者一正另一个<=0,应该算从最近的正数出发或者到最近的正数
            //我们认为刷钱点在j,当且仅当a_{j-1}与a_j构成了一个刷钱点,且a_j>0
            //其实a_j在左边也可以 
            //而且还要可以走到 
            if(flag[j]&&((j!=1&&a[i][j-1]+a[i][j]>0&&a[i][j]>=0)||(j!=m&&a[i][j]+a[i][j+1]>0&&a[i][j]>=0)))
            {
                //发现刷钱点
                p[j]=0,q[j]=k;
                last=j;
                continue; 
            }
            if(last==-inf)
                continue;
            //我们需要维护从上一个刷钱点到j还剩多少钱
            //还有从j走入上一个刷钱点在开始时至少要多少钱
            //第一点显然
            q[j]=min(q[j-1]+a[i][j],k);
            if(q[j]<0)//无法抵达j 
                q[j]=-inf;
            p[j]=max(p[j-1]-a[i][j],0);
            if(p[j]>k)
                p[j]=inf;
        }
        for(int j=1;j<=m;j++)
            if(p[j-1]<=e[i][j])
                dp[i][j]=max(dp[i][j],q[j]);
        //向右走到刷钱点同理 
        last=inf;
        for(int j=1;j<=m;j++)
            p[j]=inf,q[j]=-inf;
        for(int j=m;j>=1;j--)
        {
            if(flag[j]&&((j!=m&&a[i][j+1]+a[i][j]>0&&a[i][j]>=0)||(j!=1&&a[i][j]+a[i][j-1]>0&&a[i][j]>=0)))
            {
                p[j]=0,q[j]=k;
                last=j;
                continue;
            }
            if(last==inf)
                continue;
            q[j]=min(q[j+1]+a[i][j],k);
            if(q[j]<0)
                q[j]=-inf;
            p[j]=max(p[j+1]-a[i][j],0);
            if(p[j]>k)
                p[j]=k;
        }
        for(int j=1;j<=m;j++)
            if(p[j+1]<=e[i][j])
                dp[i][j]=max(dp[i][j],q[j]);
        //但是我们可以从一个格子走到其他地方,需要考虑走到其他地方还剩多少
        //对于每两个刷钱点之间的格子,一定从刷钱点出发最优
        int remain=-inf;
        for(int j=1;j<=m;j++)//往右走 
        {
            if((remain>=0||flag[j])&&((j!=1&&a[i][j-1]+a[i][j]>0&&a[i][j]>=0)||(j!=m&&a[i][j]+a[i][j+1]>0&&a[i][j]>=0)))
            {
                remain=k;
                dp[i][j]=max(dp[i][j],remain);
                continue;
            }
            remain=min(remain+a[i][j],k);
            remain=max(dp[i][j],remain);
            if(remain<0)
                remain=-inf;
            dp[i][j]=max(dp[i][j],remain);
        }
        remain=-inf;
        for(int j=m;j>=1;j--)//往右走 
        {
            if((remain>=0||flag[j])&&((j!=1&&a[i][j-1]+a[i][j]>0&&a[i][j]>=0)||(j!=m&&a[i][j]+a[i][j+1]>0&&a[i][j]>=0)))
            {
                remain=k;
                dp[i][j]=max(dp[i][j],remain);
                continue; 
            }
            remain=min(remain+a[i][j],k);
            remain=max(dp[i][j],remain);
            if(remain<0)
                remain=-inf;
            dp[i][j]=max(dp[i][j],remain);
        }
        for(int j=1;j<=m;j++)
        {
            int next=min(k,dp[i][j]+a[i+1][j]);
            if(next>=0)
                e[i+1][j]=next;
        }
    }
    int ans=-inf;
    for(int i=1;i<=m;i++)
        if(e[n][i]>ans)
            ans=e[n][i];
    if(ans==-inf)
        cout<<"-1\n";
    else
        cout<<ans<<"\n";
    return 0;//完结撒花 
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int ID,t;
    cin>>ID>>t; 
    for(int i=1;i<=t;i++)
        mian();
    return 0;
}