[2018.04.23 T1] 数学

本文介绍了一道关于数组划分的问题,通过动态规划求解最小价值和,并利用斜率优化技巧减少时间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

暂无链接

数学

【题目描述】

Shy 有一个长度为 n 的数组 a[i],让你把这个数组分成 k 份(每份包含至少一个元素,且每份中的元素连续)。假设其中一份长这个样子,b[1], b[2],…,b[m],那么他的价值是b[1]/b[1]+(b[1]+b[2])/b[2]+…+(b[1]+b[2]+…+b[m])/b[m],求在所有的合法的划分中,最小的价值和是多少。

【输入】

第一行两个整数 n,k 表示数组长度和分成的份数;

第二行为n个整数。

【输出】

输出一个数表示答案。(要求与标准答案的绝对或相对误差不差过 1/10000)。

【输入样例】

4 2
100 3 5 7

【输出样例】

5.7428571429

【提示】
【数据规模】

对于 30%的数据,1≤n≤1000;

对于 100%的数据,1≤n≤200000,1≤k≤min(50,n),1≤a[i]≤100000。

题解

考试的时候太懒了,写了个O(n3k)O(n3k)的暴力,A了pretestpretest连第一个档都没拿完就溜了。。。

不难看出,这是一道dpdp,我们用dp[i][j]dp[i][j]表示前ii个点切j刀的最优方案,状态转移方程很好想,我们可以枚举断点:

dp[i][j]=min(dp[k][j1]+(k+1j))dp[i][j]=min(dp[k][j−1]+代价(k+1→j))

上面的代价有个暴力的算法,设s+1s+1为整个块起点(即ss不包含在分出的块中),t为终点,sum[i]sum[i]为每个点权值val[i]val[i]的前缀和:

=i=s+1tsum[i]sum[s]val[i]代价=∑i=s+1tsum[i]−sum[s]val[i]

博主想到这里就告辞了,O(n3k)O(n3k)居然有15分,出题人可以说是很友善了,但我们可以将上面的式子进一步变形:

i=s+1tsum[i]val[i]+sum[s]×i=s+1t1val[i]∑i=s+1tsum[i]val[i]+sum[s]×∑i=s+1t1val[i]

那我们就可以预处理出sum[i]val[i]sum[i]val[i]的前缀和sdv[i]sdv[i],还有1val[i]1val[i]的前缀和rec[i]rec[i],便可以O(1)O(1)处理出贡献:

sdv[t]sdv[s]sum[s]×(rec[t]rec[s])sdv[t]−sdv[s]−sum[s]×(rec[t]−rec[s])

到此为止,我们就有30分了。。。

显然,长成这个样子,是可以斜率优化的,我们假设k<j<ik<j<i,且kkj优,为了叙述方便,我们省去第二维,用dp[i]dp[i]表示前ii个点的最优解:


dp[k]+sdv[i]sdv[k]sum[k]rec[i]+sum[k]rec[k]<dp[j]+sdv[i]sdv[j]sum[j]rec[i]+sum[j]rec[j]

dp[k]sdv[k]+sum[k]rec[k](dp[j]sdv[j]+sum[j]rec[j])sum[k]sum[j]>rec[i]dp[k]−sdv[k]+sum[k]rec[k]−(dp[j]−sdv[j]+sum[j]rec[j])sum[k]−sum[j]>rec[i]

然后,rec[i]rec[i]单增,开始打板。。。

代码
#include<bits/stdc++.h>
#define db double
#define C(x) (dp[x][t]-sdv[x]+sum[x]*rec[x])
using namespace std;
const int M=2e5+5;
int que[M],val[M],n,p;
db rec[M],sdv[M],dp[M][55],sum[M];
void in()
{
    scanf("%d%d",&n,&p);
    for(int i=1;i<=n;++i)
    {
        scanf("%d",&val[i]);
        sum[i]=val[i]+sum[i-1];
        rec[i]=1.0/val[i]+rec[i-1];
        sdv[i]=1.0*sum[i]/val[i]+sdv[i-1];
    }
}
db calc(int le,int ri){return sdv[ri]-sdv[le]-sum[le]*(rec[ri]-rec[le]);}
db slop(int t,int k,int j){return 1.0*(C(k)-C(j))/(sum[k]-sum[j]);}
void ac()
{
    for(int i=1;i<=n;++i)dp[i][1]=sdv[i];
    int le,ri,k;
    for(int i=2;i<=p;++i)
    {
        le=ri=0;
        for(int j=1;j<=n;++j)
        {
            while(le<ri&&slop(i-1,que[le+1],que[le])<rec[j])le++;
            k=que[le];
            dp[j][i]=dp[k][i-1]+calc(k,j);
            while(le<ri&&slop(i-1,que[ri],que[ri-1])>slop(i-1,j,que[ri]))ri--;
            que[++ri]=j;
        }
    }
    printf("%lf\n",dp[n][p]);
}
int main()
{
    in();ac();
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ShadyPi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值