斜率优化

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] 玩具装箱

P4360 [CEOI2004] 锯木厂选址

拆开括号,推式子后就是板子,由于斜率单调,放单调队列的板子:

// 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;
}

P5468 [NOI2019] 回家路线

发现时间一定是从前往后,把每条路线拆成发车和结束两个,发车时计算,结束时加入决策集合

\(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;
}

P5504 [JSOI2011] 柠檬

乍一看指定了区间,贡献也只能 \(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;
} 
posted @ 2024-02-14 22:21  KellyWLJ  阅读(31)  评论(0)    收藏  举报