[HNOI2008] 玩具装箱 详细讲解

题目跳转:P3195 [HNOI2008] 玩具装箱

题目大意

共有 \(n\) 个玩具,每个玩具长度 \(C_i\),将 \(j\) 至 \(i\) 连续的玩具放入一个容器 \(1\le j\le i\le n\),其长度 \(x=i-j+\sum_{k=j}^{i}C_k\)。制作一个容器的费用为 \((x-L)^2\),其中 \(L\) 为常数。求将所有玩具都放入容器中的费用最小值。

\[n\leq 5\times 10^4,1\leq L,C_i\leq10^7 \]

思路讲解与代码呈现


\(O(n^2)\) 的解法

还是一步一步来,先想暴力。

\(dp\) 设计与状态转移

设 \(dp_{i}\) 表示前 \(i\) 个玩具分好组的最小总费用。

设 \(S_i=\sum_{j=1}^{i}C_j\) 表示前 \(i\) 个玩具长度之和。

如果最后一段是第 \(j+1\) 到第 \(i\) 个玩具,那么这一段的长度为:

\[i-(j+1)+S_i-S_{(j+1)-1} \]

整理一下就是:

\[(S_i+i)-(S_j+j)-1 \]

设 \(A_i=S_i+i\),则最后一段的长度就是:

\[A_i-A_j-1 \]

那么这一段的费用就是:

\[(A_i-A_j-1-L)^2 \]

再设 \(B_j=A_j+L+1\),整理一下就是:

\[(A_i-B_j)^2 \]

所以状态转移方程就是:

\[dp_{i}=\min_{0\leq j\lt i}(dp_j+(A_i-B_j)^2) \]

要注意,因为 \(A_0=0\),\(B_0=A_0+L+1\),所以\(B_0=L+1\)。

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
uint8_t buf[1<<20],*p1,*p2;
#define gc() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<20,stdin),p1==p2)?EOF:*p1++)
ll rd(){
	ll x=0,f=1,ch=gc();
	while(ch<'0'||ch>'9'){
		if(ch=='-'){
            f=-1;
        }
        ch=gc();
	}
	while(ch>='0'&&ch<='9'){
		x=(x<<1)+(x<<3)+(ch-'0');
		ch=gc();
	}
	return x*f;
}
void write(ll x){
	if(x<0){
		x=-x;
		putchar('-');
	}
	if(x>9){
		write(x/10);
	}
	putchar(x%10+'0');
}
const ll maxn=5e4+5;
ll n,L,a[maxn],b[maxn],sum[maxn],dp[maxn];
int main(){
	n=rd();
	L=rd();
    b[0]=L+1;
	for(ll i=1;i<=n;i++){
        sum[i]=rd()+sum[i-1];
        a[i]=sum[i]+i;
        b[i]=a[i]+L+1;
        dp[i]=1e18;
	}
	for(ll i=1;i<=n;i++){
		for(ll j=0;j<i;j++){
            ll x=a[i]-b[j];
            dp[i]=min(dp[i],dp[j]+x*x);
        }
	}
	write(dp[n]);
	return 0;
}



正解:斜率优化

整理状态转移方程

把刚才的状态转移式子展开;

\[dp_{i}=\min_{0\leq j\lt i}(dp_j+A_i^2+B_j^2-2\cdot A_i\cdot B_j) \]

因为 \(A_i^2\) 与 \(j\) 的变化无关,所以提出来:

\[dp_{i}=A_i^2+\min_{0\leq j\lt i}(dp_j+B_j^2-2\cdot A_i\cdot B_j) \]

整理一下:

\[dp_{i}-A_i^2=\min_{0\leq j\lt i}(dp_j+B_j^2-2\cdot A_i\cdot B_j) \]

设 \(y=dp_i-A_i^2\),\(x=A_i\),对于每个决策 \(j\),把它看成一个一次函数:

\[y=(-2\cdot B_j)x+(dp_j+B_j^2) \]

于是问题转化为求若干直线在 \(x=A_i\) 处的最小值。

如果你做过 P10979 任务安排 2,那你肯定不陌生。没做过也没关系,可以先去我之前写的博客里补习一下:斜率优化DP例题:任务安排 三道题 详细讲解。

注意到:

  • \(C_i\geq 1\);

  • \(S_i=\sum_{j=1}^{i}C_j\),随 \(i\) 增加而增加;

  • \(A_i=S_i+i\),随 \(i\) 增加而增加;

  • \(B_i=A_i+L+1\),也随 \(i\) 增加而增加;

  • 直线的斜率是 \(-2B_j\),随 \(j\) 增加而递减。

因此可以用单调队列维护一个下凸壳。

插入新决策

设队列尾部有三个决策 \(q_{r-1},q_r,i-1\)。如果 \(q_{r}\) 已经不可能成为最优决策,就把它弹出。

判断条件为:

\[\frac{val(q_r)-val(q_{r-1})}{B_{q_r}-B_{q_{r-1}}} \geq \frac{val(i-1)-val(q_r)}{B_{i-1}-B_{q_r}} \]

其中:

\[val(x)=dp_x+B_x^2 \]

为了避免浮点误差,交叉相乘:

\[(val(q_r)-val(q_{r-1}))(B_{i-1}-B_{q_r})\\ \geq\\ (val({i-1})-val(q_r))(B_{q_r}-B_{q_{r-1}}) \]

满足时弹出 \(q_r\)。

查询最优决策

如果队列中第二个决策在当前 \(A_i\) 处已经不比第一个差,即:

\[calc(q[l+1],i)\leq calc(q[l],i) \]

那么队头决策以后也不会更优,直接弹出。

其中:

\[calc(y,x)=dp_y+(A_x-B_y)^2 \]

弹完队首后记录答案:

\[dp_i=calc(q[l],i) \]

时间复杂度

因为每个元素只入队一次,也只出队一次,所以时间复杂度为 \(O(n)\)。

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
uint8_t buf[1<<20],*p1,*p2;
#define gc() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<20,stdin),p1==p2)?EOF:*p1++)
ll rd(){
	ll x=0,f=1,ch=gc();
	while(ch<'0'||ch>'9'){
		if(ch=='-'){
            f=-1;
        }
        ch=gc();
	}
	while(ch>='0'&&ch<='9'){
		x=(x<<1)+(x<<3)+(ch-'0');
		ch=gc();
	}
	return x*f;
}
void write(ll x){
	if(x<0){
		x=-x;
		putchar('-');
	}
	if(x>9){
		write(x/10);
	}
	putchar(x%10+'0');
}
const ll maxn=5e4+5;
ll n,L,l=1,r,q[maxn],c,a[maxn],b[maxn],dp[maxn];
ll calc(ll y,ll x){
	ll squ=(a[x]-b[y]);
	return dp[y]+squ*squ;
}
bool check1(ll x,ll y,ll z){
	return calc(x,z)<=calc(y,z);
}
ll val(ll x){
    ll squ=b[x];
	return dp[x]+squ*squ;
}
bool check2(ll x,ll y,ll z){
	return (val(y)-val(x))*(b[z]-b[y])>=(val(z)-val(y))*(b[y]-b[x]);
}
int main(){
	n=rd();
	L=rd();
	b[0]=L+1;
	for(ll i=1;i<=n;i++){
		c=rd()+c;
		a[i]=c+i;
		b[i]=a[i]+L+1;
	}
	for(ll i=1;i<=n;i++){
		while(l<r&&check2(q[r-1],q[r],i-1)){
			r--;
		}
        q[++r]=i-1;
		while(l<r&&check1(q[l+1],q[l],i)){
			l++;
		}
        dp[i]=calc(q[l],i);
	}
	write(dp[n]);
	return 0;
}
posted @ 2026-11-09 21:46  beiciqi  阅读(15)  评论(0)    收藏  举报