JSOI2016 病毒感染(动态规划)

题目传送门:洛谷P5774
思路来源:郭大佬(我就是凑不要脸的白嫖党 : P)

题目分析:

我们一点点来分析这道题:

  1. 起点固定为1号,所有村庄是按线性排列的,而不是图,所以我们应该用线性DP(废话
  2. 每到达一个村庄,我们都有两种选择:一是治好这个村庄的所有人,二是直接莽到下一个村庄(当然也可以回头);而每一种操作都需要消耗一天的时间(时间是这道题最恶心的地方)
  3. 而如果我们选择莽过了一个村子,那么一旦我们在后面某一个时间选择往回走,就必须回来把这个村子给治好,并且在返回的途中也是可以顺手治好顺路的村子的(莽夫操作)
  4. 对于同一个村庄,病毒只会在自己的一座村子里扩散,而且每天领盒饭的人数都是一定的,(@某新冠病毒,你看看人家多听话 Σ( ° △ °|||)︴)

然后,沿着大佬的路子走,我们定义 dp[i] 为治好1~i个村庄所要领盒饭的人数,鉴于我们有两种操作,需要枚举中间点K,但时间效率很糟糕。所以我们必须优化。

我们都能看出来吃时间的主要是操作二,也就是计算:如果我们选择先莽过某一个村庄,然后再回来治好它,这期间领盒饭的人数;
所以,我们选择新定义一个 g[i][j] 来表示从j点一路莽到i点,再从i点回到j点治好j点,然后再回到i点,这一段时间内i到j区间内的盒饭数;

明白了g数组的含义,那么来考虑g数组的转移,依然是动态规划的老三样:

  1. 阶段肯定不用我说,就是前 i 个,我们依次遍历即可;

  2. 然后是状态,我们肯定是要枚举 j 的,那这里就要考虑到 j 的枚举顺序。由于每天每个村庄领盒饭的人都是一定的,所以我们可以通过在一开始预处理出前缀和来轻松求出区间内每天的盒饭数。那么,实际上影响总盒饭数的就只有时间这一项。然后,回归我们 g[i][j] 的定义,无论我们选择救还是莽,我们的 g[i][j] 势必要从 g[i][j+1] 转移来,为什么呢?因为 g[i][j] 是 j 到 i 区间内的盒饭数,时间也是 j 到 i 的时间,那么 j+1 在空间上离 i 更近,所以所用的时间也是 j+1 到 i 更少,我们的转移必然是从用时更少的、盒饭较少的一点点向更多的转移,反映到状态上就是我们要从倒序枚举 j ,即从 i-1 到 1 来枚举 j;

  3. 最后是转移方程:

  • 我们的决策无非两种:救 or 莽——救的话很好考虑,因为救人需要一天时间,所以后面的 j+1 到 i 的村子里的人必然要领上一波便当,直接累加一个区间的前缀和即可;如果莽的话,我们必须弄明白用时到底是多少:首先,我们要明白,对于 j 到 i 这段路程,我们来回一共走了三遍,j 到 i ,i 到 j ,j 再到 i 。然后,如果我们选择莽,第一遍从 j 到 i ,则 j 到 i 会领上好几波便当,到底几次我们想一想,或者数数手指也行,就是 i-j 对不对。
  • 第二遍从 i 回到 j ,由于我们第一遍是直接莽过来的,所以我们在回来的时候必须把路上所有村子都治好,也就是说,我们回来所用的时间应该是单纯莽的二倍,所以,再算上第一遍的,这个 j 村一共要领上 3*(i-j)波便当
  • 第三遍时从 j 村回到 i 村,由于我们已经把路上所有村子都治好了,所以 j 到 i 不会再有便当,所以 g 就不用再管了,但赶路是需要 i-j 的时间的,所以后面的 i+1 到 n 还会再 领上一波便当,也就是说,i 后面的实际上领了四波便当,(当然,这是 dp 数组的转移),我们现在先写出 g[i][j] 的转移方程:
  • g[i][j]=g[i][j+1]+Calc(j+1,i)+min((3ll*(i-j)*a[j],Calc(j+1,i))
  • 函数:Calc ( l ,r ) { return s[ r ] - s[ l -1 ] ; } s是前缀和

到这里,其实这道题就已经完成一大半了,有了 g[i][j] 作为辅助,dp[i]的转移实际上就很简单了,我们只需要在 g[i][j+1] 的基础上累加上 i 后面的部分即可,要注意的就是我上边提到的要乘4次

  • dp[i]=min(dp[i],dp[j]+g[i][j+1]+Calc(i+1,n) * ((i-(j+1)) * 4+2));
  • 另外需要注意的是我们在 j 和 j+1 来回各需要一天,最后别忘了加个2
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;
const int maxn=3000+5;
int a[maxn];
long long s[maxn],dp[maxn],g[maxn][maxn];

long long Calc(int l,int r){
    return s[r]-s[l-1];
}
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
        s[i]=a[i]+s[i-1];
    }
    memset(dp,0x3f,sizeof(dp));
    for(int i=1;i<=n;i++)
    for(int j=i-1;j>=1;j--)
        g[i][j]=g[i][j+1]+Calc(j+1,i)+min((long long)3*(i-j)*a[j],Calc(j+1,i));
    dp[0]=0;
    for(int i=1;i<=n;i++)
    for(int j=0;j<i;j++)
        dp[i]=min(dp[i],dp[j]+g[i][j+1]+Calc(i+1,n)*((i-(j+1))*4+2));
    cout<<dp[n]<<endl;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值