zoukankan      html  css  js  c++  java
  • 「算法笔记」斜率优化

    关于「斜率优化 DP」戳 这里(加了密码,暂时不公开)。

    下面都是一些斜率优化的入门题(按套路做,单调队列维护凸包),可以 从后往前看 自己推一推 QwQ。

    1. 「HNOI 2008」玩具装箱

    「HNOI 2008」玩具装箱

    Problem:给定一个长度为 (n) 的序列 (a_1,a_2,cdots,a_n) 以及常数 (m)。现要将 (a) 分成若干段,对于一段 ([l,r]),它的代价为 ((r-l+sum_{i=l}^r a_i-m)^2)。求分段的最小代价。

    (1leq nleq 5 imes 10^4,1leq m,a_ileq 10^7)

    Solution:(f_i) 表示考虑了前 (i) 个数,分成若干段的最小代价。记 (S_k=sum_{i=1}^k a_i)。那么有:

    [f_i=min_{1leq j<i}{f_j+(i-j-1+(S_i-S_j)-m)^2} ]

    为了方便化简,我们令 (G_i=S_i+i),得:

    [f_i=min_{1leq j<i}{f_j+(G_i-G_j-(m+1))^2} ]

    任取 (j,k) 且满足 (0≤k<j<i),若从 (j) 转移比 (k) 更优,则有(接下来把平方拆开然后化简即可):

    化简过程(省略部分步骤)

    一般化简大概是将 (j,k) 有关的移到右侧,与 (i) 有关的移到左侧。

    [egin{aligned} f_k+(G_i-G_k-(m+1))^2&geq f_j+(G_i-G_j-(m+1))^2\ f_k+(G_i-G_k)^2-2(G_i-G_k)(m+1)&geq f_j+(G_i-G_j)^2-2(G_i-G_j)(m+1)\ f_k-2G_iG_k+G_k^2-2(G_i-G_k)(m+1)&geq f_j-2G_iG_j+G_j^2-2(G_i-G_j)(m+1)\ 2G_iG_j-2G_iG_k+2(G_i-G_j)(m+1)-2(G_i-G_k)(m+1)&geq f_j+G_j^2-f_k-G_k^2\ 2cdot G_i(G_j-G_k)+2cdot (G_k-G_j)(m+1)&geq (f_j+G_j^2)-(f_k+G_k^2)\ 2cdot (G_j-G_k)(G_i-m-1)&geq (f_j+G_j^2)-(f_k+G_k^2) end{aligned} ]

    由于 (a_i) 为正整数,所以 (G_j>G_k),可以将 (G_j-G_k) 直接移到右侧,得到:

    [2cdot (G_i-m-1)geq frac{(f_j+G_j^2)-(f_k+G_k^2)}{G_j-G_k} ]

    [f_k+(G_i-G_k-(m+1))^2geq f_j+(G_i-G_j-(m+1))^2\ Rightarrow 2cdot (G_i-m-1)geq frac{(f_j+G_j^2)-(f_k+G_k^2)}{G_j-G_k} ]

    然后用单调队列维护一个下凸壳转移即可(维护一个斜率递增的凸壳)。

    一些解释

    具体在 之前的链接 里写过了呢。一些斜率优化最基本的东西在 这里(密码猜猜康,猜不到阔以来问我鸭)又解释了一下 QwQ。

    #include<bits/stdc++.h>
    #define int long long
    using namespace std;
    const int N=5e4+5;
    int n,m,x,s[N],g[N],f[N],q[N];
    int get(int x){
        return f[x]+g[x]*g[x];
    }
    double slope(int i,int j){
        return 1.0*(get(i)-get(j))/(g[i]-g[j]);
    }
    int sqr(int x){return x*x;}
    signed main(){
        scanf("%lld%lld",&n,&m);
        for(int i=1;i<=n;i++)
            scanf("%lld",&x),s[i]=s[i-1]+x,g[i]=s[i]+i;
        int l=0,r=0;
        for(int i=1;i<=n;i++){
            while(l<r&&slope(q[l],q[l+1])<=2*(g[i]-m-1)) l++;
            f[i]=f[q[l]]+sqr(g[i]-g[q[l]]-(m+1));
            while(l<r&&slope(q[r-1],q[r])>=slope(q[r],i)) r--;
            q[++r]=i;
        }
        printf("%lld
    ",f[n]);
        return 0;
    }

    2. 「CEOI 2004」锯木厂选址

    「CEOI 2004」锯木厂选址

    Problem:从山顶上到山底下沿着一条直线种植了 (n) 棵老树。第 (i) 棵树的重量为 (w_i),第 (i) 棵树和第 (i+1) 棵树之间的距离为 (d_i)。现在要将它们砍下来运送到锯木厂。

    木材只能朝山下运。山脚下有一个锯木厂。另外两个锯木厂将新修建在山路上。你必须决定在哪里修建这两个锯木厂,使得运输的费用总和最小。假定运输每公斤木材每米需要一分钱。

    求最小运输费用。(2leq nleq 2 imes 10^4,1leq w_ileq 10^4,0leq d_ileq 10^4)

    Solution:

    (f_i) 为以 (i) 作为第二个锯木厂的最小花费。记 (S_i)(w_i) 的前缀和,(tot)(w_i) 的总和(也就是将所有树全部运送到山脚下的费用),(dis_i)(d_i) 的后缀和。即,(S_i=sum_{j=1}^i w_j,dis_i=sum_{j=i}^n d_j)。则有:

    [f_i=min_{1leq j<i}{tot-S_j imes dis_j-(S_i-S_j) imes dis_i} ]

    一些解释

    相当于是枚举第一个锯木厂的位置 (j)。那么在 (i,j\,(j<i)) 处修了锯木厂的花费为:将 (tot) 减去 从 (j) 厂运送到山脚的额外花费 (S_j imes dis_j)(j) 处修建了锯木厂,那么 ([1,j]) 的树只需运送到 (j),不用运送到山脚下),再减去 从 (i) 厂运送到山脚的额外花费 ((S_i-S_j) imes dis_i)(j<i)(i) 处修了锯木厂,那么 ((j,i]) 的树只需运送到 (i))。

    然后这显然是可以斜率优化的,按套路来就可以了。

    (k<j<i) 时,若 (j)(k) 更优,则有:

    化简过程

    [egin{aligned} tot-S_k imes dis_k-(S_i-S_k) imes dis_i&geq tot-S_j imes dis_j-(S_i-S_j) imes dis_i\ S_j imes dis_j-S_k imes dis_k&geq (S_i-S_k) imes dis_i-(S_i-S_j) imes dis_i\ S_j imes dis_j-S_k imes dis_k&geq (S_j-S_k) imes dis_i end{aligned} ]

    (w_i) 为正整数,所以 (S_j>S_k),可以将 (S_j-S_k) 直接移到左侧:

    [frac{S_j imes dis_j-S_k imes dis_k}{S_j-S_k}geq dis_i ]

    $$ tot-S_k imes dis_k-(S_i-S_k) imes dis_igeq tot-S_j imes dis_j-(S_i-S_j) imes dis_i\ Rightarrow dis_ileq frac{S_j imes dis_j-S_k imes dis_k}{S_j-S_k} $$

    由于 (dis_i) 是递减的,我们可以维护一个上凸壳转移(维护一个斜率递减的凸壳)。

    #include<bits/stdc++.h>
    #define int long long
    using namespace std;
    const int N=2e4+5;
    int n,w[N],d[N],dis[N],s[N],tot,q[N];
    int get(int x){
        return s[x]*dis[x];
    }
    double slope(int i,int j){
        return 1.0*(get(i)-get(j))/(s[i]-s[j]);
    }
    signed main(){
        scanf("%lld",&n);
        for(int i=1;i<=n;i++)
            scanf("%lld%lld",&w[i],&d[i]);
        for(int i=n;i>=1;i--) dis[i]=dis[i+1]+d[i];
        for(int i=1;i<=n;i++) s[i]=s[i-1]+w[i],tot+=w[i]*dis[i];
        int l=0,r=0,ans=1e18;
        for(int i=1;i<=n;i++){
            while(l<r&&slope(q[l],q[l+1])>=dis[i]) l++;
            ans=min(ans,tot-s[q[l]]*dis[q[l]]-(s[i]-s[q[l]])*dis[i]);
            while(l<r&&slope(q[r-1],q[r])<=slope(q[r],i)) r--;
            q[++r]=i;
        }
        printf("%lld
    ",ans);
        return 0;
    }

    3. 「ZJOI 2007」仓库建设

    「ZJOI 2007」仓库建设

    Problem:略。

    Solution:与上一题类似。令 (f_i) 表示在 (i) 位置建设了仓库的最小代价。记 (S_i=sum_{j=1}^i p_j,G_i=sum_{j=1}^i x_jp_j)

    [f_i=min_{1leq j<i}{f_j+sum_{k=j+1}^i (x_i-x_k) imes p_k}+c_i\ Rightarrow f_i=min_{1leq j<i}{f_j+x_i(S_i-S_j)-(G_i-G_j)}+c_i ]

    一些解释

    枚举上一个仓库的位置。那么只需将 ((j,i]) 的产品运送到 (i),无需再运到山脚。

    [f_i=min_{1leq j<i}{f_j+sum_{k=j+1}^i (x_i-x_k) imes p_k}+c_i\ Rightarrow f_i=min_{1leq j<i}{f_j+x_isum_{k=j+1}^i p_k-sum_{k=j+1}x_k imes p_k}+c_i ]

    然后前缀和优化。

    然后根据套路做。当 (k<j<i) 时,若 (j)(k) 更优,则有:

    化简过程

    [egin{aligned} f_k+x_i(S_i-S_k)-(G_i-G_k)&geq f_j+x_i(S_i-S_j)-(G_i-G_j)\ x_i(S_i-S_k)-x_i(S_i-S_j)&geq f_j-f_k+(G_i-G_k)-(G_i-G_j)\ x_i(S_j-S_k)&geq (f_j+G_j)-(f_k+G_k) end{aligned} ]

    显然有 (S_j>S_k),可将 (S_j-S_k) 移到右侧:

    [x_igeq frac{(f_j+G_j)-(f_k+G_k)}{S_j-S_k} ]

    $$ f_k+x_i(S_i-S_k)-(G_i-G_k)geq f_j+x_i(S_i-S_j)-(G_i-G_j)\ Rightarrow x_igeq frac{(f_j+G_j)-(f_k+G_k)}{S_j-S_k} $$ 维护一个下凸壳转移即可(维护一个斜率递增的凸壳)。
    #define int long long
    using namespace std;
    const int N=1e6+5;
    int n,x[N],p[N],c[N],s[N],g[N],f[N],q[N];
    int get(int x){
        return f[x]+g[x];
    }
    double slope(int i,int j){
        return 1.0*(get(i)-get(j))/(s[i]-s[j]);
    }
    signed main(){
        scanf("%lld",&n);
        for(int i=1;i<=n;i++){ 
            scanf("%lld%lld%lld",&x[i],&p[i],&c[i]);
            s[i]=s[i-1]+p[i],g[i]=g[i-1]+x[i]*p[i];
        } 
        int l=0,r=0;
        for(int i=1;i<=n;i++){
            while(l<r&&slope(q[l],q[l+1])<=x[i]) l++;
            f[i]=f[q[l]]+x[i]*(s[i]-s[q[l]])-(g[i]-g[q[l]])+c[i];
            while(l<r&&slope(q[r-1],q[r])>=slope(q[r],i)) r--;
            q[++r]=i;
        }
        printf("%lld
    ",f[n]);
        return 0;
    } 

    4. 「BZOJ 1597」土地购买

    「BZOJ 1597」土地购买

    Problem:(n) 块土地,第 (i) 块土地长为 (x_i)、宽为 (y_i)。现要将这些土地划分为若干组(每块土地都应该属于且仅属于其中一组。也可以一块土地单独一组),一组土地的代价为这些土地中最大的长乘以最大的宽,即 (max{x_i} imes max{y_i})。求划分的最小代价之和。

    (1leq nleq 5 imes 10^4,1leq x_i,y_ileq 10^6)

    Solution:首先,对于土地 (i,j),若 (x_igeq x_j)(y_igeq y_j),则土地 (j) 显然是无用的(可以将 (j)(i) 分为一组,(j) 没有贡献)。

    考虑将所有土地按 (x_i) 降序为第一优先级,(y_i) 升序为第二优先级。此时对于连续的一段土地 ([l,r])(max{x_i}) 一定为 (x_l),但 (max{y_i}) 不确定。考虑通过剔除无用元素使得第二位也存在单调性,使得 (max{y_i}) 一定为 (y_r)。具体见代码。

    此时对于划分出的一段 ([l,r]),其代价为 (x_lcdot y_r)。令 (f_i) 表示按此顺序划分前 (i) 块土地的最小代价。转移时,枚举上一组土地的结尾。

    [f_i=min_{0leq j<i}{f_j+x_{j+1}cdot y_i} ]

    (k<j<i) 时,若 (j)(k) 更优,则有:

    化简过程

    [egin{aligned} f_k+x_{k+1}cdot y_i&geq f_j+x_{j+1}cdot y_i\ f_k-f_j&geq y_icdot (x_{j+1}-x_{k+1})\ end{aligned} ]

    由于 (x_i) 是降序排序的,所以 (x_{j+1}leq x_{k+1})(x_{j+1}-x_{k+1}leq 0)。将 (x_{j+1}-x_{k+1}) 移到左侧时要变号:

    [frac{f_k-f_j}{x_{j+1}-x_{k+1}}leq y_i ]

    $$ f_k+x_{k+1}cdot y_igeq f_j+x_{j+1}cdot y_i\ Rightarrow y_igeq frac{f_k-f_j}{x_{j+1}-x_{k+1}} $$

    维护下凸壳转移即可。

    #include<bits/stdc++.h>
    #define int long long
    using namespace std;
    const int N=5e4+5;
    int n,m,f[N],q[N],lst;
    bool vis[N];
    struct data{
        int x,y;
    }a[N];
    bool cmp(data x,data y){
        return x.x!=y.x?x.x>y.x:x.y<y.y;
    }
    double slope(int i,int j){
        return 1.0*(f[j]-f[i])/(a[i+1].x-a[j+1].x);
    }
    signed main(){
        scanf("%lld",&n);
        for(int i=1;i<=n;i++)
            scanf("%lld%lld",&a[i].x,&a[i].y);
        sort(a+1,a+1+n,cmp);
        for(int i=1;i<=n;i++){     //去除无贡献元素 
            if(i!=1&&a[i].x<=a[lst].x&&a[i].y<=a[lst].y) vis[i]=1;
            else lst=i;
        } 
        for(int i=1;i<=n;i++)
            if(!vis[i]) a[++m]=a[i];
        int l=0,r=0;
        for(int i=1;i<=m;i++){
            while(l<r&&slope(q[l],q[l+1])<=a[i].y) l++;
            f[i]=f[q[l]]+a[q[l]+1].x*a[i].y;
            while(l<r&&slope(q[r-1],q[r])>=slope(q[r],i)) r--;
            q[++r]=i;
        }
        printf("%lld
    ",f[m]);
        return 0;
    }

    5. 「APIO 2010」特别行动队

    「APIO 2010」特别行动队

    Problem:有一支 (n) 名士兵的部队,士兵从 (1)(n) 编号,编号为 (i) 的士兵的初始战斗力为 (x_i)。现要将他们拆分成若干组,同一组中队员的编号应该连续,即为形如 ((i,i+1,cdots,i+k)) 的序列。

    对于一个组,它的初始战斗力 (X) 为组内士兵初始战斗力之和,即 (X=x_i+x_{i+1}+cdots+x_{i+k})。它的修正战斗力为 (X'=aX^2+bX+cX),其中 (a,b,c) 是已知的系数((a<0))。

    求每组修正战斗力之和的最大值。

    (1leq nleq 10^6,-5leq aleq -1,-10^7leq b,cleq 10^7,1leq x_ileq 100)

    Solution:(f_i) 表示前 (i) 个人的战斗力之和的最大值。记 (S_i=sum_{j=1}^i x_j)

    [f_i=max_{0leq j<i}{f_j+a(S_i-S_j)^2+b(S_i-S_j)+c} ]

    (k<j<i) 时,若 (j)(k) 更优,则有:

    化简过程

    [egin{aligned} f_k+a(S_i-S_k)^2+b(S_i-S_k)+c&leq f_j+a(S_i-S_j)^2+b(S_i-S_j)+c\ f_k+a(S_i^2-2S_iS_k+S_k^2)+b(S_i-S_k)+c&leq f_j+a(S_i^2-2S_iS_j+S_j^2)+b(S_i-S_j)+c\ 2aS_iS_j-2aS_iS_k&leq (f_j+aS_j^2-bS_j)-(f_k-aS_k^2-bS_k)\ 2aS_i(S_j-S_k)&leq (f_j+aS_j^2-bS_j)-(f_k-aS_k^2-bS_k) end{aligned} ]

    因为 (x_i) 为正整数,所以 (S_j>S_k),将 (S_j-S_k) 移到右侧得:

    [2aS_ileq frac{(f_j+aS_j^2-bS_j)-(f_k-aS_k^2-bS_k)}{S_j-S_k} ]

    $$ f_k+a(S_i-S_k)^2+b(S_i-S_k)+cleq f_j+a(S_i-S_j)^2+b(S_i-S_j)+c\ Rightarrow 2aS_ileq frac{(f_j+aS_j^2-bS_j)-(f_k-aS_k^2-bS_k)}{S_j-S_k} $$

    不等式左侧是单调递减的(题目中保证 (a<0),且显然 (S_i) 递增),右侧分母上的前缀和是单调递增的。

    维护上凸壳转移即可(斜率递减),每次的最优决策就是队首。

    #include<bits/stdc++.h>
    #define int long long
    using namespace std;
    const int N=1e6+5;
    int n,a,b,c,x[N],s[N],q[N],f[N];
    int sqr(int x){return x*x;} 
    int get(int x){
        return f[x]+a*sqr(s[x])-b*s[x];
    }
    double slope(int i,int j){
        return 1.0*(get(i)-get(j))/(s[i]-s[j]);
    }
    signed main(){
        scanf("%lld%lld%lld%lld",&n,&a,&b,&c);
        for(int i=1;i<=n;i++)
            scanf("%lld",&x[i]),s[i]=s[i-1]+x[i];
        int l=0,r=0;
        for(int i=1;i<=n;i++){
            while(l<r&&slope(q[l],q[l+1])>=2*a*s[i]) l++;
            f[i]=f[q[l]]+a*sqr(s[i]-s[q[l]])+b*(s[i]-s[q[l]])+c;
            while(l<r&&slope(q[r-1],q[r])<=slope(q[r],i)) r--;
            q[++r]=i;
        } 
        printf("%lld
    ",f[n]);
        return 0;
    } 

    6. 「APIO 2014」序列分割

    「APIO 2014」序列分割

    Problem:给出一个长度为 (n) 的非负整数序列 ({a_n})。先要将序列分为 (k+1) 个非空的块。为了得到 (k+1) 块,你需要重复下面的操作 (k) 次:

    1. 选择一个有超过一个元素的块(初始时你只有一块,即整个序列);
    2. 选择两个相邻元素把这个块从中间分开,得到两个非空的块。

    每次操作后将获得那两个新产生的块的元素和的乘积的分数。最大化最后的总得分,要求输出方案。

    (2leq nleq 10^5,1leq kleq min(n-1,200),0leq a_ileq 10^4)

    Solution:

    我们首先证明答案和分割顺序无关。

    如果我们有长度为 (3) 的序列 (x,y,z) 将其分为 (3) 部分,有如下两种分割方法:

    1. 先在 (x) 后面分割,答案为 (x(y+z)+yz) 即为 (xy+yz+zx)
    2. 先在 (y) 后面分割,答案为 ((x+y)z+xy) 即为 (xy+yz+zx)

    然后这个结论可以扩展到任意长度的序列(分析一下贡献),证毕。

    (F_{i,j}) 表示前 (i) 个数进行 (j) 次分割的最大得分。记 (S_i)(a_i) 的前缀和。

    [F_{i,k}=max_{0leq j<i}{F_{j,k-1}+S_j(S_i-S_j)} ]

    为了方便表述,记 (F_{i,k})(f_i)(F_{j,k-1})(g_j),相当于把 (F) 的第二维滚动掉了。

    [f_i=max_{0leq j<i}{g_j+S_j(S_i-S_j)} ]

    (k<j<i) 时,若 (j)(k) 更优,则有:

    化简过程

    [egin{aligned} g_k+S_k(S_i-S_k)&leq g_j+S_j(S_i-S_j)\ S_kS_i-S_jS_i&leq (g_j-S_j^2)-(g_k-S_k^2)\ S_i(S_k-S_j)&leq (g_j-S_j^2)-(g_k-S_k^2) end{aligned} ]

    显然 (S_jgeq S_k),所以 (S_k-S_jleq 0),将 (S_k-S_j) 移到右边需变号:

    [S_igeq frac{(g_j-S_j^2)-(g_k-S_k^2)}{S_k-S_j} ]

    $$ g_k+S_k(S_i-S_k)leq g_j+S_j(S_i-S_j)\ Rightarrow frac{(g_j-S_j^2)-(g_k-S_k^2)}{S_k-S_j}leq S_i $$

    维护下凸壳转移即可。然后输出方案的话记一个 (pre) 表示从哪里转移过来。

    #include<bits/stdc++.h>
    #define int long long
    using namespace std;
    const int N=1e5+5,M=210;
    int n,k,a[N],s[N],g[N],f[N],pre[N][M],q[N],x;
    int get(int x){
        return g[x]-s[x]*s[x];
    }
    double slope(int i,int j){
        if(s[i]==s[j]) return -1e18;    //注意此题中,a[i] 为非负整数,可能会取到 0,所以 s[k]-s[j] 的值可能为 0。这种情况需特判,slope 需返回 -inf(否则算斜率的时候除以 0 就挂了)。
        return 1.0*(get(i)-get(j))/(s[j]-s[i]);
    }
    signed main(){
        scanf("%lld%lld",&n,&k);
        for(int i=1;i<=n;i++) 
            scanf("%lld",&a[i]),s[i]=s[i-1]+a[i];
        for(int j=1;j<=k;j++){
            int l=0,r=0;
            for(int i=1;i<=n;i++){
                while(l<r&&slope(q[l],q[l+1])<=s[i]) l++;
                f[i]=g[q[l]]+s[q[l]]*(s[i]-s[q[l]]),pre[i][j]=q[l];
                while(l<r&&slope(q[r-1],q[r])>=slope(q[r],i)) r--;
                q[++r]=i; 
            }
            memcpy(g,f,sizeof(f));
        }
        printf("%lld
    ",f[n]),x=n;
        for(int i=k;i>=1;i--)
            x=pre[x][i],printf("%lld%c",x,i==1?'
    ':' ');
        return 0;
    }

    7. 「SDOI 2016」征途

    「SDOI 2016」征途

    Problem:给出一个有 (n) 个数的序列 ({a_n}),现要把其分为 (m) 段,设每段的权值为该段 (a_i) 之和,最小化这 (m) 段的方差 (v),输出 (v imes m^2)

    (1leq nleq 3000,sum a_ileq 30000)

    Solution:与上一题类似。

    (b_i) 为每段的权值,(overline b) 为平均数,我们要最小化:

    [frac{sum_{i=1}^m(b_i-overline b)^2}{m} imes m^2 ]

    将平方拆开,得到:

    [m imes left(sum_{i=1}^m(b_i^2-2b_ioverline b+overline b^2) ight) ]

    继续化简,并代入 (overline b=frac{sum_{i=1}^mb_i}{m}),得到:

    化简过程

    [egin{aligned} &=m imes sum_{i=1}^m b_i^2-m imes 2overline b imes sum_{i=1}^mb_i+m imes (moverline b^2)\ &=m imes sum_{i=1}^m b_i^2-m imes 2 imes frac{sum_{i=1}^mb_i}{m} imes sum_{i=1}^mb_i+m imes left(m imes{left(frac{sum_{i=1}^mb_i}{m} ight)}^2 ight)\ &=m imes sum_{i=1}^m b_i^2-2 imes left(sum_{i=1}^mb_i ight)^2+m^2 imes frac{left(sum_{i=1}^mb_i ight)^2}{m^2}\ &=m imes sum_{i=1}^m b_i^2-2 imes left(sum_{i=1}^mb_i ight)^2+left(sum_{i=1}^mb_i ight)^2\ &=m imes sum_{i=1}^m b_i^2-left(sum_{i=1}^mb_i ight)^2 end{aligned} ]

    [m imes sum_{i=1}^m b_i^2-left(sum_{i=1}^mb_i ight)^2 ]

    发现后面那部分的 (left(sum_{i=1}^mb_i ight)^2) 等于 (left(sum_{i=1}^n a_i ight)^2) 为定值,我们现在要最小化 (sum_{i=1}^m b_i^2)

    (F_{i,j}) 表示前 (i) 个数分为 (j) 段的最小值。记 (S_i)(a_i) 的前缀和。

    [F_{i,k}=max_{0leq j<i}{F_{j,k-1}+(S_i-S_j)^2} ]

    (F) 的第二维用滚动数组滚掉。记 (F_{i,k})(f_i)(F_{j,k-1})(g_j)

    [f_i=max_{0leq j<i}{g_j+(S_i-S_j)^2} ]

    (k<j<i) 时,若 (j)(k) 更优,则有:

    [egin{aligned} g_k+(S_i-S_k)^2&geq g_j+(S_i-S_j)^2\ 2S_i(S_j-S_k)&geq (g_j+S_j^2)-(g_k+S_k^2)\ 2S_i&geq frac{(g_j+S_j^2)-(g_k+S_k^2)}{S_j-S_k} end{aligned} ]

    维护下凸壳转移即可。

    #include<bits/stdc++.h>
    #define int long long
    using namespace std;
    const int N=3e4+5;
    int n,m,x,s[N],f[N],g[N],q[N];
    int sqr(int x){return x*x;} 
    int get(int x){
        return g[x]+sqr(s[x]);
    }
    double slope(int i,int j){
        return 1.0*(get(i)-get(j))/(s[i]-s[j]);
    }
    signed main(){
        scanf("%lld%lld",&n,&m);
        for(int i=1;i<=n;i++)
            scanf("%lld",&x),s[i]=s[i-1]+x,g[i]=s[i]*s[i];
        for(int j=2;j<=m;j++){
            int l=0,r=0;
            for(int i=1;i<=n;i++){
                while(l<r&&slope(q[l],q[l+1])<=2*s[i]) l++;
                f[i]=g[q[l]]+sqr(s[i]-s[q[l]]);
                while(l<r&&slope(q[r-1],q[r])>=slope(q[r],i)) r--;
                q[++r]=i;
            }
            memcpy(g,f,sizeof(f));
        }
        printf("%lld
    ",m*f[n]-sqr(s[n]));
        return 0;
    } 

    8. 「Codeforces 311B」Cats Transport

    「Codeforces 311B」Cats Transport

    Problem:小 S 是农场主,他养了 (m) 只猫,雇了 (p) 位饲养员。农场中有一条笔直的路,路边有 (n) 座山,从 (1)(n) 编号。第 (i) 座山与第 (i?1) 座山之间的距离是 (d_i)。饲养员都住在 (1) 号山上。

    有一天,猫出去玩。第 (i) 只猫去 (h_i) 号山玩,玩到时刻 (t_i) 停止,然后在原地等饲养员来接。饲养员们必须回收所有的猫。每个饲养员沿着路从 (1) 号山走到 (n) 号山,把各座山上已经在等待的猫全部接走。饲养员在路上行走需要时间,速度为 (1) 米每单位时间。饲养员在每座山上接猫的时间可以忽略,可以携带的猫的数量为无穷大。

    你的任务是规划每个饲养员从 (1) 号山出发的时间,使得所有猫等待时间的总和尽量小。饲养员出发的时间可以为负。

    (2leq nleq 10^5,1leq mleq10^5,1leq pleq 100)

    Solution:(a_i=t_i-sum_{j=2}^{h_i}d_j),也就是让第 (i) 只猫不等待的出发时间。如果有人从 (T) 时刻出发,那么等待时间为 (T-a_i)

    考虑将 (a_i) 从小到大排序,那么每一个饲养员应该会带走一段连续区间的猫。

    (F_{i,k}) 表示 (k) 个饲养员带走前 (i) 只小猫的最少等待时间。记 (S_i)(a_i) 的前缀和。

    [F_{i,k}=min_{1leq j<i}{F_{j,k-1}+a_i(i-j)-(S_i-S_j)} ]

    即,第 (k) 个饲养员带走 ((j,i]) 的小猫,那么就在 (a_i) 出发,等待时间为 (a_i(i-1)-(S_i-S_j))

    用滚动数组将 (F) 的第二维滚掉。设 (f_i=F_{i,k},g_j=F_{j,k-1})。则:

    [f_i=min_{1leq j<i}{g_j+a_i(i-j)-(S_i-S_j)} ]

    (k<j<i) 时,若 (j)(k) 更优,则有:

    化简过程

    [egin{aligned} g_k+a_i(i-k)-(S_i-S_k)&geq g_j+a_i(i-j)-(S_i-S_j)\ a_i(j-k)&geq (g_j+S_j)-(g_k+S_k)\ a_i&geq frac{(g_j+S_j)-(g_k+S_k)}{j-k} end{aligned} ]

    [g_k+a_i(i-k)-(S_i-S_k)geq g_j+a_i(i-j)-(S_i-S_j)\ Rightarrow a_igeq frac{(g_j+S_j)-(g_k+S_k)}{j-k} ]

    维护下凸壳转移即可。

    #include<bits/stdc++.h>
    #define int long long
    using namespace std;
    const int N=1e5+5;
    int n,m,p,d[N],a[N],h[N],t[N],s[N],f[N],g[N],q[N];
    int get(int x){
        return g[x]+s[x];
    }
    double slope(int i,int j){
        return 1.0*(get(i)-get(j))/(i-j);
    }
    signed main(){
        scanf("%lld%lld%lld",&n,&m,&p);
        for(int i=2;i<=n;i++)
            scanf("%lld",&d[i]),d[i]+=d[i-1];
        for(int i=1;i<=m;i++)
            scanf("%lld%lld",&h[i],&t[i]),a[i]=t[i]-d[h[i]];
        sort(a+1,a+1+m);
        for(int i=1;i<=m;i++) s[i]=s[i-1]+a[i];
        memset(g,0x3f,sizeof(f)),g[0]=0;
        for(int j=1;j<=p;j++){
            int l=0,r=0;
            for(int i=1;i<=m;i++){ 
                while(l<r&&slope(q[l],q[l+1])<=a[i]) l++;
                f[i]=g[q[l]]+a[i]*(i-q[l])-(s[i]-s[q[l]]);
                while(l<r&&slope(q[r-1],q[r])>=slope(q[r],i)) r--;
                q[++r]=i;
            } 
            memcpy(g,f,sizeof(f));
        }
        printf("%lld
    ",f[m]);
        return 0;
    }

    9. 「SDOI 2012」任务安排

    「SDOI 2012」任务安排

    Problem:(n) 个任务,标号为 (1)(n),第 (i) 个任务单独完成所需的时间是 (t_i)。要求将 (n) 个任务分为若干批,每批包含相邻的若干任务。在每批任务开始前,机器需要启动时间 (s),完成这批任务所需的时间是各个任务需要时间的总和。

    同一批任务将在同一时刻完成。每个任务的费用是它的完成时刻乘以一个费用系数 (c_i)

    求最小总费用。(1leq nleq 3 imes 10^5,1leq sleq 2^8,|t_i|leq 2^8,0leq c_ileq 2^8)

    Solution:(f_{i,j}) 表示前 (i) 个任务被分为 (j) 批的最小费用。记 (T_i) 表示 (t_i) 的前缀和,(C_i) 表示 (c_i) 的前缀和。

    [f_{i,j}=min_{0leq k<i}{f_{k,j-1}+(T_i+s imes j)(C_i-C_k)} ]

    意思就是,第 (j) 批任务(包含 ((k,i]) 的任务)的完成时间为 (T_i+s imes j),这批任务的费用和为 ((T_i+s imes j) imes sum_{p=k+1}^i c_p)

    注意到转移已经是 (mathcal O(1)) 的了,考虑优化 DP 的状态。

    发现 (j) 的作用仅是为了计算 (j) 批任务的启动时间和 (s imes j)。将当前这批任务(前 (i) 个任务分完了)分出后,会增加 (s) 等待的启动时间,则后续费用和会增加 (sum_{p=i+1}^n c_p imes s)。考虑提前加进去,从而优化掉状态的第二维。这就是“费用提前计算”的思想。

    [egin{aligned} f_i&=min_{0leq j<i}{f_j+T_i imes (C_i-C_j)+s imes (C_n-C_j)}\ &=min_{0leq j<i}{f_j-T_i imes C_j-s imes C_j}+T_i imes C_i+s imes C_n\ end{aligned} ]

    (k<j<i) 时,若 (j)(k) 更优,则有:

    [egin{aligned} f_k-T_i imes C_k-s imes C_k&geq f_j-T_i imes C_j-s imes C_j\ Rightarrow T_i&geq frac{(f_j-s imes C_j)-(f_k-s imes C_k)}{C_j-C_k} end{aligned} ]

    直接单调队列维护下凸壳:

    int l=0,r=0;
    for(int i=1;i<=n;i++){
        while(l<r&&slope(q[l],q[l+1])<=t[i]) l++;
        f[i]=f[q[l]]+t[i]*(c[i]-c[q[l]])+s*(c[n]-c[q[l]]);
        while(l<r&&slope(q[r-1],q[r])>=slope(q[r],i)) r--; 
        q[++r]=i;
    }

    然而这样是错的。注意到 (t_i) 可能为负,会导致 (t_i) 的前缀和 (T_i) 不一定单调,这影响了最优决策点的选择,无法使用单调队列取队首 选择最优决策点

    因此不能弹出队首,而是维护整个凸壳,每次查询最优决策点时在凸壳上二分,找到第一个使得左侧斜率小于 (T_i),右侧斜率不小于 (T_i) 的位置即为最优决策点。

    #include<bits/stdc++.h>
    #define int long long
    using namespace std;
    const int N=3e5+5;
    int n,s,x,y,t[N],c[N],f[N],q[N];
    int get(int x){
        return f[x]-s*c[x];
    }
    double slope(int i,int j){
        if(c[i]==c[j]) return 1e18;
        return 1.0*(get(i)-get(j))/(c[i]-c[j]);
    }
    int find(int l,int r,int v){
        int ans=r;
        while(l<=r){
            int mid=(l+r)/2;
            if(slope(q[mid],q[mid+1])>=v) ans=mid,r=mid-1;
            else l=mid+1;
        }
        return q[ans];
    }
    signed main(){
        scanf("%lld%lld",&n,&s);
        for(int i=1;i<=n;i++){ 
            scanf("%lld%lld",&x,&y);
            t[i]=t[i-1]+x,c[i]=c[i-1]+y;
        } 
        int l=0,r=0;
        for(int i=1;i<=n;i++){
            int pos=find(l,r,t[i]);
            f[i]=f[pos]+t[i]*(c[i]-c[pos])+s*(c[n]-c[pos]);
            while(l<r&&slope(q[r-1],q[r])>=slope(q[r],i)) r--; 
            q[++r]=i;
        }
        printf("%lld
    ",f[n]);
        return 0;
    }
    转载请注明原文链接
  • 相关阅读:
    侧滑界面的实现
    Private field 'XXX' is never assigned的解决办法
    android先加载注册页面而不是MainActivity主页面
    每日日报4
    每日日报3
    47 选择排序和插入排序
    计算机启动过程 BIOS MBR等
    ARM中MMU地址转换理解(转)
    深度学习框架 CatBoost 介绍
    预训练词嵌入
  • 原文地址:https://www.cnblogs.com/maoyiting/p/14537557.html
Copyright © 2011-2022 走看看