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

斜率优化训练记录

前言

斜率优化一般用于优化dp的转移,借着训练斜率优化的相关问题来提升一些DP思维。选择老学长留下的专题场来练手,由于该场题数较多,以及个人不太愿意长时间进行单一专题训练,因此开此文来记录断续的训练结果和心得。

记录

0x01

由一道简单入门题玩具装箱开头,题意和思路比较简单就不讲了。
代码

#include
#define dd(x) cout<<#x<<" = "<K(q[t-1],i))
            t--;
        q[++t]=i;
    }
    cout<

0x02

小A与最大字段和还是入门题。题意见原题面,比较简单。思路的话,维护一个普通前缀和与一个梯形前缀和,然后与上题一样通过变形式子写成直线截距式。唯一的不同是,该题Ai可能是负数,因此直线的斜率不能保证单调变化,因此选取最优点时需要二分队列找到首个往后比直线斜率小的点。(上一题由于直线斜率单调增加,因此每次选最优点只要把队首比直线斜率小的点都出队即可)。然后这题要最大值,所以入队时要维护一个上凸壳。
代码

#include
#define dd(x) cout<<#x<<" = "<K(q[t],q[t-1]))
            --t;
        q[++t]=i;
    }
    cout<

0x03

HDU2993这题思路不是很难,但是不知道为什么卡读入,相当恶心,比较low的IO优化还过不去,我T了十几发之后用了学长的fread读入的板子才过的。因此极其不建议大家做,脑内AC就可以了。
题意是给个长为n的数列,找一个长度不小于k且平均值最大的子段。换句话说就是找所有点(i,sum[i])中斜率最大的两个点的斜率。
不难想到,我们应该维护一个下凸壳(因为其上方的点肯定无法与之后的点构成更优的解),然后一般可以二分找最优点,但是由这题的性质可以发现,因为sum[i]是递增的,因此每次找到更优的点时,其前面的点可以直接舍弃(它们不可能与后面新加入的点构成更大的斜率了)。因此复杂度可以做到O(N)。
代码(再次强调,不建议做)

#include
#define dd(x) cout<<#x<<" = "<

0x04

HDU 3045,注意一下转移的合法性,其余的就是常规的斜率优化DP。特别注意斜率的比较上不要写错(手贱做差时取了绝对值,WA了十几发),为了避免精度误差,建议写成乘法形式来比较。
代码

#include
#define dd(x) cout<<#x<<" = "< P;
typedef priority_queue BQ;
typedef priority_queue,greater > SQ;
const int maxn=5e5+10,mod=1e9+7,INF=0x3f3f3f3f;
ll a[maxn],sum[maxn],f[maxn];
int q[maxn],h,t;
ll dy(int i,int j)
{
    return f[i]-sum[i]+i*a[i+1]-f[j]+sum[j]-j*a[j+1];
}
ll dx(int i,int j)
{
    return a[i+1]-a[j+1];
}
ll getf(int i,int j)
{
    return f[i]+sum[j]-sum[i]-a[i+1]*(j-i);
}
int main()
{
    int n,k;
    while (scanf("%d%d",&n,&k)!=EOF)
    {
        for (int i=1;i<=n;++i)
            scanf("%I64d",&a[i]);
        sort(a+1,a+1+n);
        for (int i=1;i<=n;++i)
            sum[i]=sum[i-1]+a[i];
        h=t=0;
        for (int i=k;i<=n;++i)
        {
            int j=i-k;
            if (j>=k)
            {
                while (h

0x05

POJ 1180,挺好的一题。题意比较繁琐,建议直接看原题面。
这题用斜率优化的部分也很常规,比较有技巧的是如何把枚举分组数的这一维优化掉,使得复杂度降到O(N)。可以发现,要知道当前这个组的结束时间,与之前分了几个组有关,这样的话转移时就必须枚举了。但是换个角度,我们可以考虑每个分组对后面分组代价的影响,提前计算对当前组造成的s对全局答案的贡献,这样转移时就不需要考虑前面分组对当前的影响了(因为已经在前面计算过了)。因此转移方程可以写成f(j)=min{f(i)+(tsum[j]+s)*(fsum[j]-fsum[i])+s*(fsum[n]-fsum[j])}(0<=i

#include
#include
#include
#define dd(x) cout<<#x<<" = "< P;
const int maxn=1e5+10,mod=1e9+7,INF=0x3f3f3f3f;
ll fsum[maxn],tsum[maxn],dp[maxn];
int q[maxn],h,t,s,n;
inline ll dy(int i,int j)
{
    return dp[i]-dp[j];
}
inline ll dx(int i,int j)
{
    return fsum[i]-fsum[j];
}
inline ll getv(int i,int j)
{
    return dp[i]+(tsum[j]+s)*(fsum[j]-fsum[i])+s*(fsum[n]-fsum[j]);
}
int main()
{
    while (cin>>n)
    {
        cin>>s;
        for (int i=1;i<=n;++i)
        {
            scanf("%lld%lld",&tsum[i],&fsum [i]);
            tsum[i]+=tsum[i-1],fsum[i]+=fsum[i-1];
        }
        t=h=0;
        for (int j=1;j<=n;++j)
        {
            while (h

0x06

HDU 3480,相比于第一题玩具装箱,多了分组数的限制,其他是一模一样的。出于空间的限制,我们先枚举分组数,这样每次队列就可以清空复用。注意分组数为k时的答案都是由分组数k-1时的答案转移而来,注意边界点,细节见代码

#include
#define dd(x) cout<<#x<<" = "< P;
typedef priority_queue BQ;
typedef priority_queue,greater > SQ;
const int maxn=1e4+10,mod=1e9+7,INF=0x3f3f3f3f;
ll a[maxn],f[maxn>>1][maxn];
int q[maxn],h,t;
inline ll dy(int i,int j,int k)
{
    return f[k][i]+a[i+1]*a[i+1]-f[k][j]-a[j+1]*a[j+1];
}
inline ll dx(int i,int j)
{
    return a[i+1]-a[j+1];
}
inline ll getf(int i,int j,int k)
{
    return f[k-1][i]+(a[j]-a[i+1])*(a[j]-a[i+1]);
}
int main()
{
    int T;
    cin>>T;
    for (int cas=1;cas<=T;++cas)
    {
        int n,m;
        scanf("%d%d",&n,&m);
        for (int i=1;i<=n;++i)
            scanf("%lld",&a[i]);
        sort(a+1,a+1+n);
        for (int j=1;j<=n;++j)
            f[1][j]=(a[j]-a[1])*(a[j]-a[1]);
        for (int k=2;k<=m;++k)
        {
            h=t=0;
            q[0]=k-1;
            for (int j=k;j<=n;++j)
            {
                while (hn?0ll:f[m][n]);
    }
    return 0;
}

0x07

HDU 2829,与上题一个类型,这题定义一个区间的价值为其中任意两个数乘积的和。在区间价值的表达式上想了比较久,区间[i,j]的价值可以表示为C[j]-C[i]-sum[i]*(sum[j]-sum[i]),C[i]表示前缀i的价值,想出来之后就是跟上题一样的处理,没什么坑点,注意边界就行。
代码

#include
#define dd(x) cout<<#x<<" = "< P;
typedef priority_queue BQ;
typedef priority_queue,greater > SQ;
const int maxn=1e3+10,mod=1e9+7,INF=0x3f3f3f3f;
ll f[maxn][maxn],a[maxn],sum[maxn],c[maxn];
int q[maxn],h,t;
inline ll dy(int i,int j,int k)
{
    return f[k][i]+sum[i]*sum[i]-c[i]-f[k][j]-sum[j]*sum[j]+c[j];
}
inline ll dx(int i,int j)
{
    return sum[i]-sum[j];
}
inline ll getf(int i,int j,int k)
{
    return f[k-1][i]+c[j]-c[i]-(sum[j]-sum[i])*sum[i];
}
int main()
{
    int n,m;
    while (scanf("%d%d",&n,&m)&&n)
    {
        ++m;
        for (int i=1;i<=n;++i)
        {
            scanf("%lld",&a[i]);
            sum[i]=sum[i-1]+a[i];
        }
        for (int i=2;i<=n;++i)
            c[i]=c[i-1]+sum[i-1]*a[i];
        for (int j=1;j<=n;++j)
            f[1][j]=c[j];
        for (int k=2;k<=m;++k)
        {
            h=t=0;
            q[0]=k-1;
            for (int j=k;j<=n;++j)
            {
                while (h

0x08

HDU 3507,简化版的玩具装箱,没有长度限制也没有分组数限制,入门级别
代码

#include
#define dd(x) cout<<#x<<" = "< P;
typedef priority_queue BQ;
typedef priority_queue,greater > SQ;
const int maxn=5e5+10,mod=1e9+7,INF=0x3f3f3f3f;
ll f[maxn],a[maxn],sum[maxn];
int q[maxn],h,t,M;
inline ll dy(int i,int j)
{
    return f[i]+sum[i]*sum[i]-f[j]-sum[j]*sum[j];
}
inline ll dx(int i,int j)
{
    return sum[i]-sum[j];
}
inline ll getf(int i,int j)
{
    return f[i]+(sum[j]-sum[i])*(sum[j]-sum[i])+M;
}
int main()
{
    int n;
    while (scanf("%d%d",&n,&M)!=EOF)
    {
        for (int i=1;i<=n;++i)
        {
            scanf("%lld",&a[i]);
            sum[i]=sum[i-1]+a[i];
        }
        h=t=0;
        for (int i=1;i<=n;++i)
        {
            while (h

0x09

POJ 3709,有长度限制,无分组数限制,常规解法

#include
#include
#include
#define dd(x) cout<<#x<<" = "< P;
const int maxn=5e5+10,mod=1e9+7,INF=0x3f3f3f3f;
ll f[maxn],a[maxn],sum[maxn];
int q[maxn],h,t,M;
inline ll dy(int i,int j)
{
    return f[i]-sum[i]+i*a[i+1]-f[j]+sum[j]-j*a[j+1];
}
inline ll dx(int i,int j)
{
    return a[i+1]-a[j+1];
}
inline ll getf(int i,int j)
{
    return f[i]+sum[j]-sum[i]-(j-i)*a[i+1];
}
int main()
{
    int T;
    cin>>T;
    while (T--)
    {
        int n,k;
        scanf("%d%d",&n,&k);
        for (int i=1;i<=n;++i)
        {
            scanf("%lld",&a[i]);
            sum[i]=sum[i-1]+a[i];
        }
        h=t=0;
        for (int i=k;i<=n;++i)
        {
            int j=i-k;
            if (j>=k)
            {
                while (h

0x0A

HDU 3669,每个人有高h和宽w,人可以过门仅当高和宽均不大于门的高H和宽W。要造不超过k道门,每道门确定一个W和H,每道门的造价是W*H,使得所有人都能通过至少一道门,求最少造价。为了方便计算,我们进行一定的预处理,我们按人的其中一个属性升序排列,得到的序列再按另一个属性降序排列(即剔除那些h和w均小于某人的人,显然他们不会影响答案),这时得到的序列,满足第一维递增,第二维递减。显然进同一道门的人在序列里肯定是连续的。跑一个DP来进行分组,斜率优化一下转移就做完了。
预处理细节见代码

#include
#define fi first
#define se second
#define dd(x) cout<<#x<<" = "<=a[cnt].se)
                --cnt;
            a[++cnt]=a[i];
        }
        m=min(m,cnt);
        for (int i=1;i<=cnt;++i)
            f[1][i]=a[i].fi*a[1].se;
        for (int k=2;k<=m;++k)
        {
            h=t=0;
            q[0]=k-1;
            for (int j=k;j<=cnt;++j)
            {
                while (h

0x0B

311B - Cats Transport,挺好的一题,要先处理出接每只猫的最早出发时间ai,对这个时间排序,贪心思想可知每个饲养员肯定要负责接连续一段的猫。于是对这个数组a进行dp分组,斜率优化。

#include
#define dd(x) cout<<#x<<" = "< P;
typedef priority_queue BQ;
typedef priority_queue,greater > SQ;
const int maxn=1e5+10,mod=1e9+7,INF=0x3f3f3f3f;
ll a[maxn],f[105][maxn],sumd[maxn],suma[maxn]; 
int q[maxn],h,t;
ll dy(int i,int j,int k)
{
    return f[k][i]+suma[i]-f[k][j]-suma[j];
}
ll dx(int i,int j)
{
    return i-j;
}
ll getf(int i,int j,int k)
{
    return f[k-1][i]+a[j]*(j-i)-suma[j]+suma[i];
}
int main()
{
    int n,m,p;
    cin>>n>>m>>p;
    for (int i=2;i<=n;++i)
    {
        scanf("%I64d",&sumd[i]);
        sumd[i]+=sumd[i-1];
    }
    for (int i=1;i<=m;++i)
    {
        ll h,t;
        scanf("%I64d%I64d",&h,&t);
        a[i]=t-sumd[h];
    }
    sort(a+1,a+1+m);
    for (int i=1;i<=m;++i)
    {
        suma[i]=suma[i-1]+a[i];
        f[1][i]=i*a[i]-suma[i];
    }
    for (int k=2;k<=p;++k)
    {
        h=t=0;
        q[0]=k-1;
        for (int j=k;j<=m;++j)
        {
            while (h

0x0C

货币兑换Cash,由贪心的思想可知,每次买卖一定会把钱花完或把金券卖完。因此f(i)表示到第i天手里最多有多少钱,f(i)=max(A(j)*a[i]+B(j)*b[i]),其中A(j)表示第j天把钱全部花完可以获得的A金券数量,B(j)同理。稍作变形得-B(j)=a[i]/b[i]*A(j)-f[i]/b[i]。可以发现横坐标A(j)和斜率a[i]/b[i]都不单调。
这种情况可以用平衡树维护动态凸包或者cdq分治来解决。我这里选用了CDQ来解决这个问题。
大概思想是,对于每一个f(i),它的决策点一定在它左边,换句话说每个点只会影响它右边的点。对于求解区间[l,r]内的f值,可以先递归求解其左区间,完成后左区间的f则处理完毕,然后对于左区间的点重新排序维护一个凸包,对于右区间的点可以二分这个凸包找决策点进行更新,然后再递归求解右区间内部的影响,完成后则右区间的f值也处理完毕。此时这个大区间的f值处理完毕。

#include
#define dd(x) cout<<#x<<" = "<

你可能感兴趣的:(斜率优化系列——训练记录)