「算法笔记」斜率优化

关于「斜率优化 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\)。求分段的最小代价。

\(1\leq n\leq 5\times 10^4,1\leq m,a_i\leq 10^7\)

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

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

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

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

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

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

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

\[\begin{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\\ 2\cdot G_i(G_j-G_k)+2\cdot (G_k-G_j)(m+1)&\geq (f_j+G_j^2)-(f_k+G_k^2)\\ 2\cdot (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\) 直接移到右侧,得到:

\[2\cdot (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))^2\geq f_j+(G_i-G_j-(m+1))^2\\ \Rightarrow 2\cdot (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\n",f[n]);
    return 0;
}

2. 「CEOI 2004」锯木厂选址

「CEOI 2004」锯木厂选址

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

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

求最小运输费用。\(2\leq n\leq 2\times 10^4,1\leq w_i\leq 10^4,0\leq d_i\leq 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_{1\leq j<i}\{tot-S_j\times dis_j-(S_i-S_j)\times dis_i\} \]

一些解释

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

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

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

化简过程

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

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

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

$$ tot-S_k\times dis_k-(S_i-S_k)\times dis_i\geq tot-S_j\times dis_j-(S_i-S_j)\times dis_i\\ \Rightarrow dis_i\leq \frac{S_j\times dis_j-S_k\times 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\n",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_{1\leq j<i}\{f_j+\sum_{k=j+1}^i (x_i-x_k)\times p_k\}+c_i\\ \Rightarrow f_i=\min_{1\leq j<i}\{f_j+x_i(S_i-S_j)-(G_i-G_j)\}+c_i \]

一些解释

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

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

然后前缀和优化。

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

化简过程

\[\begin{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_i\geq \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_i\geq \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\n",f[n]);
    return 0;
} 

4. 「BZOJ 1597」土地购买

「BZOJ 1597」土地购买

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

\(1\leq n\leq 5\times 10^4,1\leq x_i,y_i\leq 10^6\)

Solution:首先,对于土地 \(i,j\),若 \(x_i\geq x_j\)\(y_i\geq 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_l\cdot y_r\)。令 \(f_i\) 表示按此顺序划分前 \(i\) 块土地的最小代价。转移时,枚举上一组土地的结尾。

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

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

化简过程

\[\begin{aligned} f_k+x_{k+1}\cdot y_i&\geq f_j+x_{j+1}\cdot y_i\\ f_k-f_j&\geq y_i\cdot (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_i\geq f_j+x_{j+1}\cdot y_i\\ \Rightarrow y_i\geq \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\n",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\))。

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

\(1\leq n\leq 10^6,-5\leq a\leq -1,-10^7\leq b,c\leq 10^7,1\leq x_i\leq 100\)

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

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

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

化简过程

\[\begin{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_i\leq \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)+c\leq f_j+a(S_i-S_j)^2+b(S_i-S_j)+c\\ \Rightarrow 2aS_i\leq \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\n",f[n]);
    return 0;
} 

6. 「APIO 2014」序列分割

「APIO 2014」序列分割

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

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

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

\(2\leq n\leq 10^5,1\leq k\leq \min(n-1,200),0\leq a_i\leq 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_{0\leq 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_{0\leq j<i}\{g_j+S_j(S_i-S_j)\} \]

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

化简过程

\[\begin{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_j\geq S_k\),所以 \(S_k-S_j\leq 0\),将 \(S_k-S_j\) 移到右边需变号:

\[S_i\geq \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\n",f[n]),x=n;
    for(int i=k;i>=1;i--)
        x=pre[x][i],printf("%lld%c",x,i==1?'\n':' ');
    return 0;
}

7. 「SDOI 2016」征途

「SDOI 2016」征途

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

\(1\leq n\leq 3000,\sum a_i\leq 30000\)

Solution:与上一题类似。

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

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

将平方拆开,得到:

\[m\times \left(\sum_{i=1}^m(b_i^2-2b_i\overline b+\overline b^2)\right) \]

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

化简过程

\[\begin{aligned} &=m\times \sum_{i=1}^m b_i^2-m\times 2\overline b\times \sum_{i=1}^mb_i+m\times (m\overline b^2)\\ &=m\times \sum_{i=1}^m b_i^2-m\times 2\times \frac{\sum_{i=1}^mb_i}{m}\times \sum_{i=1}^mb_i+m\times \left(m\times{\left(\frac{\sum_{i=1}^mb_i}{m}\right)}^2\right)\\ &=m\times \sum_{i=1}^m b_i^2-2\times \left(\sum_{i=1}^mb_i\right)^2+m^2\times \frac{\left(\sum_{i=1}^mb_i\right)^2}{m^2}\\ &=m\times \sum_{i=1}^m b_i^2-2\times \left(\sum_{i=1}^mb_i\right)^2+\left(\sum_{i=1}^mb_i\right)^2\\ &=m\times \sum_{i=1}^m b_i^2-\left(\sum_{i=1}^mb_i\right)^2 \end{aligned} \]

\[m\times \sum_{i=1}^m b_i^2-\left(\sum_{i=1}^mb_i\right)^2 \]

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

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

\[F_{i,k}=\max_{0\leq 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_{0\leq j<i}\{g_j+(S_i-S_j)^2\} \]

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

\[\begin{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\n",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\) 号山出发的时间,使得所有猫等待时间的总和尽量小。饲养员出发的时间可以为负。

\(2\leq n\leq 10^5,1\leq m\leq10^5,1\leq p\leq 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_{1\leq 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_{1\leq j<i}\{g_j+a_i(i-j)-(S_i-S_j)\} \]

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

化简过程

\[\begin{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_i\geq \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\n",f[m]);
    return 0;
}

9. 「SDOI 2012」任务安排

「SDOI 2012」任务安排

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

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

求最小总费用。\(1\leq n\leq 3\times 10^5,1\leq s\leq 2^8,|t_i|\leq 2^8,0\leq c_i\leq 2^8\)

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

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

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

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

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

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

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

\[\begin{aligned} f_k-T_i\times C_k-s\times C_k&\geq f_j-T_i\times C_j-s\times C_j\\ \Rightarrow T_i&\geq \frac{(f_j-s\times C_j)-(f_k-s\times 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\n",f[n]);
    return 0;
}
posted @ 2021-03-15 14:56  maoyiting  阅读(147)  评论(0编辑  收藏  举报