斜率优化dp
斜率优化dp
【模板】任务安排
机器上有 n 个需要处理的任务,它们构成了一个序列。这些任务被标号为 1 到 n,因此序列的排列为 1 , 2 , 3 .... n。这 n 个任务被分成若干批,每批包含相邻的若干任务。从时刻 0 开始,这些任务被分批加工,第 i 个任务单独完成所需的时间是 T_i。在每批任务开始前,机器需要启动时间 s,而完成这批任务所需的时间是各个任务需要时间的总和。
同一批任务将在同一时刻完成。每个任务的费用是它的完成时刻乘以一个费用系数 C_i。
请确定一个分组方案,使得总费用最小。
先设计 \(dp\) 状态,显然可以 \(dp_{i,j}\) 表示把前 \(i\) 个任务分配进前 \(j\) 组里,令 \(t_i\) 表示时间的前缀和数组,\(c_i\) 表示贡献的前缀和数组,转移的时候枚举第 \(j-1\) 个区间的终点 \(k\),\(dp_{i,j}=\min({dp_{k,j-1}+(j*s+t_i)*(c_i-c_k)})\) ,其中 \(j*s\) 表示前 \(j\) 个区间中重新启动产生的总贡献,\(t_i\) 即为花费的时间,\(c_i-c_k\) 为价值和。
\(O(n^3)\) 时间空间全寄,考虑优化。发现如果想把 \(j\) 这一维缩掉,转移方程中唯一与他有关的就是重新启动的贡献 \(s*j\)。那么我们可以换一种统计方式,不在枚举每个 \(i\) 时统计前面所有段对当前段的的贡献,我们直接在统计当前段(新开段)的时候提前把他对后面产生的贡献加上,这样与一开始的转移方程是等价的。
这种思想是一种名为费用提前计算的经典思想。
新开的转移方程为 \(dp_i\) 表示前 \(i\) 个任务最小贡献,前缀和意义不变,依然枚举上一个区间的终点 \(k\),\(dp_i=\min (p_j+(c_n-c_k)*s+t_i*(c_i-c_k))\),其中 \((c_n-c_k)*s\) 表示分段后他对于后面每一段都贡献了一个 \(s\)。
现在时间是 \(O(n^2)\) 的了,可以通过弱化弱化版。
考虑怎么在缩减时间复杂度,变形一下柿子,将与 \(i\) 有关(常量)与和 \(j\) 相关(变量)分别放在等号两侧,整理为
\(dp_i-(t_i*c_i+c_n*s)=(s+t_i)*c_j-dp_j\)
观察到它非常像直线的点斜式,\(b=y-kx\),其中 \(b\) 是要求的 \(dp_i\),而 \(y\) 是关于 \(j\) 的 \(dp_j\),\(kx\) 则是关于 \(i\) 和 \(j\) 乘起来的柿子,\(x\) 为 \(c_j\)。
发现 \(x\),\(y\) 与当前选的 \(i\) 无关,可以抽象成在平面直角坐标系上的点。而过一点的一个一次函数知道它的斜率就能唯一确定,斜率 \(k\) 由 \(i\) 决定,所以可以暴力枚举点(要转移的 \(j\)),因为 \(k\) 已知,所以直线与 \(y\) 的截距(转移下来的 \(dp_i\))唯一确定,更新最小值即可。
这种枚举也是 \(O(n^2)\) 的,但是这种优化就显而易见了。\(k\) 已知,就可以想象成一条直线不断向上平移,截距的最小值即为这条直线上第一个碰到的点的坐标。该坐标前的所有坐标斜率一定小于 \(k\),后面的一定大于 \(k\)。而如果斜率 \(k\) 是具有单调性的(随着 \(i\) 增大 \(k\) 增大或减少),那么就可以用单调队列维护,新插入第 \(i\) 个点的时候维护队列,\(O(1)\) 转移,总复杂度 \(O(n)\)。
这样可以过掉弱化版。
但这道题的 \(k\) 不符合单调性,仅凸包符合单调性的话,那么取得时候就不能仅考虑队头,要维护整个凸包,不够小出队的依旧,转移时可以二分转移的最优决策点,转移 \(O(\log n)\) 的,总复杂度 \(O(n \log n)\) 的,可以通过。
code
// //"那你既然选择了这条路 就坚持走下去吧"
#include <bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f3f3f
#define int long long
#define ull unsigned long long
#define MAXN 400019
#define WA cerr<<"CCF\n";
#define eps 1e-5
#define none -1145141919
#define pii pair<int,int>
#define Y cout<<"Yes\n"
#define N cout<<"No\n"
#define H cout<<"\n"
#define WH cerr<<"\n"
#define pb push_back
#define pk pop_back
#define lf(_n) fixed<<setprecision()<<_n
const int MOD=1e9+7;
int n;
int dp[MAXN];
int s,t[MAXN],c[MAXN];
int qx[MAXN*2],qy[MAXN*2];
int ed=0;
bool ck(int mid,int k)
{
return (qy[mid+1]-qy[mid])>(k*(qx[mid+1]-qx[mid]));
}
int ef(int tot,int k)
{
int L=0,R=tot,mid,res=0;
while(L<=R)
{
mid=(L+R)/2;
if(ck(mid,k)) { R=mid-1; res=mid; }
else { L=mid+1; }
}
return res;
}
main()
{
cin>>n>>s;
for(int i=1;i<=n;i++) { cin>>t[i]>>c[i]; t[i]+=t[i-1],c[i]+=c[i-1]; }
for(int i=1;i<=n;i++)
{
int k=(s+t[i]);//要平移的线
//单调队列维护斜率>=k的最小斜率
/*while(hd<ed&&(qy[hd+1]-qy[hd])<(k*(qx[hd+1]-qx[hd]))) ++hd;*/
int h=ef(ed,k);
int nx=qx[h],ny=qy[h];
dp[i]=ny-k*nx+(t[i]*c[i]+c[n]*s);
nx=c[i];
ny=dp[i];
while(0<ed&&((qx[ed-1]-qx[ed])*(qy[ed]-ny))<=((qy[ed-1]-qy[ed])*(qx[ed]-nx))) --ed;
qx[++ed]=nx; qy[ed]=ny;
}
cout<<dp[n];
return 0;
}

浙公网安备 33010602011771号