以 P5785 [SDOI2012]任务安排 为例
朴素方程
其实也没那么简单,第一眼想法是设 \(f(i,k)\) 表示以 \(i\) 为结尾,共分了 \(k\) 段的总方案数
\[f(i,k)=\min_{j=0}^{i-1} f(j,k-1)+(sumc_i-sumc_j)\cdot( sumt_i+s\cdot k) \]枚举的 \(j\) 表示当前选的第 \(k\) 段为 \((j,i]\)
\(O(n^3)\) 连弱化版也过不去
考虑题中没有指定 \(k\) 这一维,一般可以优化掉
用携带代价的思想,发现除了启动时间外,每次时刻恒定,提前计算启动时间对后面的影响,即 \(s\cdot(sumc_n-sumc_j)\)
\[f(i)=\min_{j=0}^{i-1} f(j)+(sumc_i-sumc_j)\cdot sumt_i+s\cdot (sumc_n-sumc_j) \]\(O(n^2)\),可过 P2365 任务安排
斜率优化
考虑快速找到这个最优的 \(j\),分离含有 \(j\) 的变量
\[f(i)=\min_{j=0}^{i-1} f(j)-(sumt_i+s)\cdot sumc_j+sumc_i\cdot sumt_i+s\cdot sumc_n \]假设我们已找到那个最有的 \(j\),去掉 \(\min\)
\[f(i)=f(j)-(sumt_i+s)\cdot sumc_j+sumc_i\cdot sumt_i+s\cdot sumc_n \]移项,
\[f(j)=(sumt_i+s)\cdot sumc_j+f(i)-sumc_i\cdot sumt_i+s\cdot sumc_n \]我们想最小化的是 \(f(i)\),发现
设 \(y=f(j),k=sumt_i+s\)
\(x=sumc_j,b=f(i)-sumc_i\cdot sumt_i+s\cdot sumc_n\)
则式子写成直线 \(y=kx+b\) 的形式!
\((sumc_i,f(j))\) 看作点,\(k\) 在当前为定值,相当于一条斜率固定直线,从下往上移,\(b\) 不断增大,直到碰到某个点为止
要让 \(b\) 最小,只有下凸壳上的点有贡献,维护下凸壳
同理,要让 \(b\) 最大,只有上凸壳上的点有贡献,维护上凸壳
下凸壳上,每段斜率单调递增
上凸壳上,每段斜率单调递减
我们想找的那个最优的点,查询的斜率 \(k\) 在它左右两段斜率之间
细节
这题没有就这么结束
1. 单调问题
插入点横坐标单调,询问斜率是否单调
-
如果每次询问的 \(k\) 单调,那最优的就方便查询,用单调队列或单调栈维护,队头斜率比 \(k\) 小弹出,队尾插入,直接查询队头,单调栈同理,一般复杂度 \(O(n)\)
-
但这题时间 \(t_i\) 可能为负
真的离谱,\(k\) 不单调,此时就需要用单调栈维护出整个凸包,在凸包上二分找出这个点,一般复杂度 \(O(n\log n)\)
这还好,但麻烦的是下面的情况
插入点横坐标不单调,询问斜率单调
我们的想法是动态维护这个凸包
但很复杂,要用平衡树且我不会
一般这不是考察的重点,那我们只能曲线救国,转化为横坐标单调的情况
以这题为例,如果 \(c_i<0,t_i>0\)
则倒着 DP,设 \(f(i)\) 表示处理完 \([i,n]\) 的最小费用
\[f(i)=\min_{j=i+1}^n f(j)+(s+sumt_{j-1}-sumt_{i-1})\cdot(sumc_n-sumc_{i-1}) \]\[f(i)=\min_{j=i+1}^n f(j)+(sumc_n-sumc_{i-1})\cdot sumt_{j-1}+(s-sumt_{i-1})\cdot (sumc_n-sumc_{i-1}) \]\[f(j)=(-sumc_n+sumc_{i-1})\cdot sumt_{j-1}+f(i)-(s-sumt_{i-1})\cdot (sumc_n-sumc_{i-1}) \]发现 \(x=sumt_{j-1}\),又是单调的了!
2. 实现问题
这里涉及斜率的比较,如果做除法会除以 0 ,还可能有精度问题
因此这里比较斜率均转化为乘法
但是万一有毒瘤出题人,乘法会爆 long long,还是老老实实的除
再就是一些维护凸包的细节:
-
凸包中至少有两个点,当只有一个点时不弹出,二分时特判一下
-
如果两段斜率相同,则先加入的一段也要弹出,防止二分出错
-
二分实现是找斜率最小的且比 \(k\) 大的一段,注意这个点代表了它后面的一段
补充:
判断凸包可以用叉积,具体的见 计算几何
精度更高,但上下凸壳别弄反了
代码
inline ll merge(ll id)
{
ll l = 1, r = top;
if(l == r) return q[r];
while(l < r)
{
ll mid = (l + r) >> 1;
if((f[q[mid + 1]] - f[q[mid]]) > (s + sumt[id]) * (sumc[q[mid + 1]] - sumc[q[mid]])) r = mid;
else l = mid + 1;
}
return q[r];
}
int main()
{
n = read(), s = read();
for(reg ll i = 1; i <= n; ++i)
{
t[i] = read(), c[i] = read();
sumt[i] = sumt[i - 1] + t[i], sumc[i] = sumc[i - 1] + c[i];
}
memset(f, 0x3f, sizeof(f)), f[0] = 0, q[++top] = 0;
for(reg ll i = 1; i <= n; ++i)
{
// f[i] = min(f[i], f[j] + (sumc[n] - sumc[j]) * s + (sumc[i] - sumc[j]) * sumt[i]);
ll id = merge(i);
f[i] = f[id] + (sumc[n] - sumc[id]) * s + (sumc[i] - sumc[id]) * sumt[i];
while(top > 1 && (f[i] - f[q[top]]) * (sumc[q[top]] - sumc[q[top - 1]]) <= (f[q[top]] - f[q[top - 1]]) * (sumc[i] - sumc[q[top]]))
--top;
q[++top] = i;
}
printf("%lld", f[n]);
return 0;
}
更多应用
拆开括号,推式子后就是板子,由于斜率单调,放单调队列的板子:
// P3195 [HNOI2008] 玩具装箱
inline ll sqr(ll p) {return p * p;}
inline ll x(ll p) {return p + s[p];}
inline ll y(ll p) {return f[p] + sqr(x(p));}
inline double slope(ll a, ll b)
{
if(x(a) == x(b)) return -inf;
return (double)(y(a) - y(b)) * (double)1.0 / (x(a) - x(b));
}
int main()
{
n = read(), l = read();
for(ll i = 1; i <= n; ++i) c[i] = read(), s[i] = s[i - 1] + c[i];
q[++tt] = 0;
for(ll i = 1; i <= n; ++i)
{
while(hh < tt && slope(q[hh + 1], q[hh]) <= (double)2.0 * (i + s[i] - l - 1)) ++hh;
f[i] = f[q[hh]] + sqr(i + s[i] - l - 1 - x(q[hh]));
while(hh < tt && slope(q[tt], q[tt - 1]) >= slope(i, q[tt])) --tt;
q[++tt] = i;
}
printf("%lld", f[n]);
return 0;
}
发现时间一定是从前往后,把每条路线拆成发车和结束两个,发车时计算,结束时加入决策集合
设 \(f(i)\) 为走完第 \(i\) 条线路后的最小烦躁值,最后把总用时的部分加上
新增从 \(1\to 1\),起止时间均为 \(0\) 的路线,方便转移
看到二次式,拆开,推式子
\[f_i=\min_{j}f_j+A(p_i-q_j)^2+B(p_i-q_j)+C \]\[f_i=f_j+Ap_i^2+Aq_j^2+2Ap_iq_j+Bp_i-Bq_j+C \]\[f_i=f_j+Aq_j^2-Bq_j+2Ap_iq_j+Ap_i^2+Bp_i+C \]设 \(b=f_i,y=f_j+Aq_j^2-Bq_j,k=2Ap_i,x=q_j\)
则刚好写成斜率优化的形式!
注意线路端点必须相接,由于按时间排序后斜率单调递增,因此用 deque
存储,把每个插入进终点站对应的队列中
我不知道小猫烦不烦燥,反正我调试时挺烦躁的
ll y(ll i) {return f[i] + a * trn[i].q * trn[i].q - b * trn[i].q;}
pll mins(ll i, ll j) {return mp(trn[i].q - trn[j].q, y(i) - y(j));}
ll cross(pll i, pll j) {return i.fi * j.se - i.se * j.fi;}
void ins(ll nw, ll i)
{
if(f[i] > 1e18) return;
while(que[nw].size() > 1)
{
ll t1 = que[nw].back(); que[nw].popb(); ll t2 = que[nw].back();
if(cross(mins(i, t1), mins(i, t2)) < 0) {que[nw].pb(t1); break;}
}
que[nw].pb(i);
}
void calc(ll i)
{
if(que[trn[i].st].empty()) return;
ll nw = trn[i].st;
while(que[nw].size() > 1)
{
ll t1 = que[nw].front(); que[nw].popf(); ll t2 = que[nw].front();
if(cross(mins(t2, t1), mp(1, 2ll * a * trn[i].p)) < 0) {que[nw].pf(t1); break;}
}
ll pos = que[nw].front();
f[i] = y(pos) - 2ll * a * trn[i].p * trn[pos].q + c + a * trn[i].p * trn[i].p + b * trn[i].p;
}
int main()
{
n = read(), m = read(), a = read(), b = read(), c = read();
trn[1].st = trn[1].ed = 1, trn[1].p = trn[1].q = 0;
for(int i = 2; i <= m + 1; ++i) f[i] = inf;
f[1] = 0;
ins(trn[1].ed, 1);
for(int i = 2; i <= m + 1; ++i)
{
trn[i].st = read(), trn[i].ed = read(), trn[i].p = read(), trn[i].q = read();
tim[trn[i].p].pb(mp(i, 0)), tim[trn[i].q].pb(mp(i, 1));
}
for(ll i = 0; i <= 40000; ++i)
{
for(pll j : tim[i])
if(j.se) ins(trn[j.fi].ed, j.fi);
for(pll j : tim[i])
if(!j.se) calc(j.fi);
}
for(int i = 1; i <= m + 1; ++i)
if(trn[i].ed == n) ans = min(ans, f[i] + trn[i].q);
printf("%lld", ans);
return 0;
}
乍一看指定了区间,贡献也只能 \(O(n)\) 算
但是可以排除一定不优的情况:
如果这个区间选择 \(x\),且区间不以 \(x\) 为开头结尾,那么把区间不是 \(x\) 的开头结尾去掉,这个区间的贡献不变,且会产生新增贡献,比原来更优
因此,取的数一定是区间的开头结尾
那么与上题处理位置的方法一样,插入至当前的后面一个数对应的 deque
,查询则在当前的数对应的 deque
中查询
ll x(ll i) {return s[i][1];}
ll y(ll i) {return f[i] + a[i + 1] * s[i][1] * s[i][1];}
pll mins(ll i, ll j) {return mp(x(i) - x(j), y(i) - y(j));}
ll dot(pll i, pll j) {return i.fi * j.fi + i.se * j.se;}
ll cross(pll i, pll j) {return i.fi * j.se - i.se * j.fi;}
void calc(ll i)
{
ll id = a[i], k = 2ll * a[i] * s[i][0];
while(q[id].size() > 1 && cross(mins(q[id].back(), q[id][q[id].size() - 2]), mp(1, k)) >= 0)
q[a[i]].popb();
f[i] = y(q[id].back()) - k * x(q[id].back()) + a[i] * s[i][0] * s[i][0];
}
void ins(ll i)
{
ll id = a[i + 1];
while(q[id].size() > 1 && cross(mins(i, q[id].back()), mins(q[id].back(), q[id][q[id].size() - 2])) <= 0)
q[id].popb();
q[id].pb(i);
}
int main()
{
n = read();
for(int i = 1; i <= n; ++i) a[i] = read();
for(int i = 1; i <= n; ++i)
{
++buc[a[i]];
s[i][0] = buc[a[i]], s[i][1] = buc[a[i + 1]];
}
q[a[1]].pb(0);
for(int i = 1; i <= n; ++i)
{
if(q[a[i]].empty()) continue;
calc(i);
if(i < n) ins(i);
}
printf("%lld", f[n]);
return 0;
}
标签:return,trn,cdot,优化,ll,sumt,斜率,sumc
From: https://www.cnblogs.com/KellyWLJ/p/18015726