最全的斜率优化/Convex Hull Trick题单及思路(日更)

第一题(入门题)

319C - Kalila and Dimna in the Logging Industry

题意:有 n n n棵树,每棵树有一个高度 a i a_i ai,每次操作可以使一棵树高度减1,每次操作完需要充电,充电只能在树的高度为0的地方(这个位置的树被砍完了),花费为 b i b_i bi,保证 a a a数组升序且 a 1 = 1 a_1=1 a1=1 b b b数组降序且 b n = 0 b_n=0 bn=0,求把所有树都砍完的最小花费。

思路:考虑到 b n = 0 b_n=0 bn=0,那么我们肯定是前面选几棵树砍完,然后砍第 n n n棵树,再利用第 n n n个位置一直充电,免费地把剩下所有的树砍完。所以考虑 d p i dp_i dpi表示砍倒第 i i i棵树的最小花费,最后输出 d p n dp_n dpn即可。显然,转移如下:
d p i = m i n ( d p j + a i ∗ b j )      ( j < i ) dp_i=min(dp_j+a_i*b_j)\;\;(j<i) dpi=min(dpj+aibj)(j<i)
显然是一个斜率优化的形式,进行转化(省略取min):
d p i = b j ∗ a i + d p j dp_i=b_j*a_i+dp_j dpi=bjai+dpj
{ y = d p i k = b j      x = a i      b = d p j \left \{ \begin{array}{c} y=dp_i \\ k=b_j\;\;\\ x=a_i\;\;\\ b=dp_j \end{array} \right. y=dpik=bjx=aib=dpj
化简成 y = k x + b y=kx+b y=kx+b的形式,发现是一个最纯朴的形式 😃 ,直接CHT

struct CHT {
    struct line {
        ll k, b;
        line() {}
        line(ll k, ll b): k(k), b(b) {}
        
        double intersect(line l) {
            double db = l.b - b;
            double dk = k - l.k;
            return db / dk;
        }
        
        ll operator () (ll x) {
            return k * x + b;
        }
    };
    
    vector<double> x;
    vector<line> li;
    
    void init(line l) {
        x.push_back(-LINF);
        li.push_back(l);
    }
    
    void addLine(line l) {
        while (li.size() >= 2 && l.intersect(li[li.size() - 2]) <= x.back()) {
            x.pop_back();
            li.pop_back();
        }
        if (!li.empty()) {
            x.push_back(l.intersect(li.back()));
        }
        li.push_back(l);
    }
    
    ll query(ll qx) {
        int id = upper_bound(x.begin(), x.end(), qx) - x.begin();
        --id;
        return li[id](qx);
    }
};

ll dp[maxn],a[maxn],b[maxn];

void solve(){
	int n;
	cin>>n;
	rep(i,1,n) cin>>a[i];
	rep(i,1,n) cin>>b[i];
	
	mem(dp,0x3f);
	dp[1]=0;
	CHT cht;
	cht.init(CHT::line(b[1],dp[1]));
	
	rep(i,2,n){
		dp[i]=cht.query(a[i]);
		cht.addLine(CHT::line(b[i],dp[i]));
	}
	cout<<dp[n]<<endl;
}

第二题

洛谷P3195 [HNOI2008]玩具装箱

题意: n n n个物品,每个物品有一个价值 C i C_i Ci,把这n个物品分成若干段,每段 ( l , r ) (l,r) (l,r)的价值为 ( ∑ i = l r C i + j − i + 1 + L ) 2 (\sum_{i=l}^rC_i+j-i+1+L)^2 (i=lrCi+ji+1+L)2,求分割后最小的价值总和。

dp方程:
d p i = m i n ( d p j + ( p r e i − p r e j + i − j − L ) 2 )      ( j < i ) dp_i=min(dp_j+(pre_i-pre_j+i-j-L)^2)\;\;(j<i) dpi=min(dpj+(preiprej+ijL)2)(j<i)
A = p r e i + i      ,    B = p r e j + j + L            A 只与 i 有关, B 只与 j 有关 A=pre_i+i\;\;,\;B=pre_j+j+L \;\;\;\;\;A只与i有关,B只与j有关 A=prei+i,B=prej+j+LA只与i有关,B只与j有关
那么我们的 d p 方程就可以化为 ( 省略了取 m i n ) : 那么我们的dp方程就可以化为(省略了取min): 那么我们的dp方程就可以化为(省略了取min)

d p i = d p j + ( A − B ) 2 = d p j + A 2 + B 2 − 2 A B \begin{aligned}dp_i&=dp_j+(A-B)^2\\ &=dp_j+A^2+B^2-2AB \end{aligned} dpi=dpj+(AB)2=dpj+A2+B22AB
再化成斜率优化的形式:
d p i − A 2 = − 2 A B + d p j + B 2 dp_i-A^2=-2AB+dp_j+B^2 dpiA2=2AB+dpj+B2
{ y = d p i − A 2 k = − 2 B              x = A                        b = d p j + B 2 \left \{ \begin{array}{c} y=dp_i-A^2 \\ k=-2B\;\;\;\;\;\;\\ x=A \;\;\;\;\;\;\;\;\;\;\;\\ b=dp_j+B^2 \end{array} \right. y=dpiA2k=2Bx=Ab=dpj+B2
原式子已经化成了y=kx+b的形式,接下来只需要利用CHT实现即可。

struct CHT {
    struct line {
        ll k, b;
        line() {}
        line(ll k, ll b): k(k), b(b) {}
        
        double intersect(line l) {
            double db = l.b - b;
            double dk = k - l.k;
            return db / dk;
        }
        
        ll operator () (ll x) {
            return k * x + b;
        }
    };
    
    vector<double> x;
    vector<line> li;
    
    void init(line l) {
        x.push_back(-1e30);
        li.push_back(l);
    }
    
    void addLine(line l) {
        while (li.size() >= 2 && l.intersect(li[li.size() - 2]) <= x.back()) {
            x.pop_back();
            li.pop_back();
        }
        if (!li.empty()) {
            x.push_back(l.intersect(li.back()));
        }
        li.push_back(l);
    }
    
    ll query(ll qx) {
        int id = upper_bound(x.begin(), x.end(), qx) - x.begin();
        --id;
        return li[id](qx);
    }
};

int n;
ll L,a[maxn],pre[maxn],dp[maxn];
void solve(){
	cin>>n>>L;
	L++;
	rep(i,1,n) cin>>a[i],pre[i]=pre[i-1]+a[i];
	CHT cht;

	dp[0]=0;
	cht.init(CHT::line(-2*(a[0]+L),dp[0]+(pre[0]+L)*(pre[0]+L)));
		
	rep(i,1,n){
		dp[i]=cht.query(pre[i]+i)+(pre[i]+i)*(pre[i]+i);
		cht.addLine(CHT::line( -2*(pre[i]+i+L), dp[i]+ (pre[i]+i+L) * (pre[i]+i+L) ) );
	}
	cout<<(long long)dp[n]<<endl;
}

第三题

1083E - The Fair Nut and Rectangles

题意:给定 n ( n < = 1 e 6 ) n(n<=1e6) n(n<=1e6)个矩形 每个矩形的左下角位置 ( 0 , 0 ) (0,0) (0,0),右上角 ( x i , y i ) (x_i,y_i) (xi,yi),并且有一个花费 c i c_i ci,所有矩形没有嵌套,选一些矩形,使得他们面积的交 减去 花费的和最大。

思路:首先,矩形没有嵌套,那么当我们将所有矩形右上角按 x x x小到大排序后, y y y一定是大到小排序的。那么假如当前要选第 i i i个矩形,上一个选的矩形是第 j j j个,那么我们的总价值改变为,如图: + ( x i − x j ) ∗ y i − a i +(x_i-x_j)*y_i-a_i +(xixj)yiai
在这里插入图片描述
所以我们就能得到一个dp式:
d p i = m a x ( d p j + ( x i − x j ) ∗ y i − a i ) ( j < i ) dp_i=max(dp_j+(x_i-x_j)*y_i-a_i) (j<i) dpi=max(dpj+(xixj)yiai)(j<i)
看到这样形式的式子,很容易想到斜率优化,于是化简式子至一下形式:
d p i − x i ∗ y i + a i = − x j ∗ y i + d p j dp_i-x_i*y_i+a_i=-x_j*y_i+dp_j dpixiyi+ai=xjyi+dpj

{ y = d p i − x i ∗ y i + a i k = − x j                                          x = y i                                                b = d p j                                            \left \{ \begin{array}{c} y&=dp_i-x_i*y_i+a_i \\ k&=-x_j\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\ \\ x&=y_i \;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\; \\ b&=dp_j\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\ \end{array} \right. ykxb=dpixiyi+ai=xj =yi=dpj 
原式子已经化成了 y = k x + b y=kx+b y=kx+b的形式,接下来只需要利用CHT实现即可。

struct CHT {
    struct line {
        ll k, b;
        line() {}
        line(ll k, ll b): k(k), b(b) {}
        
        double intersect(line l) {
            double db = l.b - b;
            double dk = k - l.k;
            return db / dk;
        }
        
        ll operator () (ll x) {
            return k * x + b;
        }
    };
    
    vector<double> x;
    vector<line> li;
    
    void init(line l) {
        x.push_back(-LINF);
        li.push_back(l);
    }
    
    void addLine(line l) {
        while (li.size() >= 2 && l.intersect(li[li.size() - 2]) <= x.back()) {
            x.pop_back();
            li.pop_back();
        }
        if (!li.empty()) {
            x.push_back(l.intersect(li.back()));
        }
        li.push_back(l);
    }
    
    ll query(ll qx) {
        int id = upper_bound(x.begin(), x.end(), qx) - x.begin();
        --id;
        return li[id](qx);
    }
};

struct rectangle{
	ll x,y,a;
	bool operator < (const rectangle& ts) const{
		return x<ts.x;
	} 
}s[1000005];
int n;

ll dp[1000005];
void solve(){
	cin>>n;
	rep(i,1,n) cin>>s[i].x>>s[i].y>>s[i].a;
	sort(s+1,s+1+n);
	
	CHT cht;
	cht.init(CHT::line(0,0));
	
	ll mx=0;
	rep(i,1,n){
		dp[i]=-cht.query(-s[i].y)-s[i].a+s[i].x*s[i].y;
		cht.addLine(CHT::line(-s[i].x,-dp[i]));
		mx=max(mx,dp[i]);
	}
	cout<<mx<<endl;
}

第四题

631E - Product Sum

题意:给定一个数组 a a a,可以进行最多一次操作:将一个数插到另一个位置,相当于选一个区间循环移位一下,求最大的 ∑ i = 1 n a i ∗ i \sum_{i=1}^na_i*i i=1naii

假设循环移位的区间为 ( i , j ) (i,j) (i,j),发现 i i i左边的价值和 j j j右边的价值并没有改变,只有中间这部分有改变,因此我们只计算最大的改变量,具体需要分两种情况讨论:
(1)右移:
请添加图片描述

(2)左移:
请添加图片描述
tips:本题比较特殊,因为等式左边原本就没有含 i , j i,j i,j的项,所以化简后的式子中带有 i 和 j i和j ij的项,可以任取一个作为 k k k,另一个作为 x x x,但是由于一个是单调的,另一个不是单调的,所以我们要取单调的那个作为我们的 k k k,不然就得cdq分治了= =
最后两个公式分别做两次CHT即可 😃

struct CHT {
    struct line {
        ll k, b;
        line() {}
        line(ll k, ll b): k(k), b(b) {}
        
        double intersect(line l) {
            double db = l.b - b;
            double dk = k - l.k;
            return db / dk;
        }
        
        ll operator () (ll x) {
            return k * x + b;
        }
    };
    
    vector<double> x;
    vector<line> li;
    
    void init(line l) {
    	x.clear();
    	li.clear();
        x.push_back(-LINF);
        li.push_back(l);
    }
    
    void addLine(line l) {
        while (li.size() >= 2 && l.intersect(li[li.size() - 2]) <= x.back()) {
            x.pop_back();
            li.pop_back();
        }
        if (!li.empty()) {
            x.push_back(l.intersect(li.back()));
        }
        li.push_back(l);
    }
    
    ll query(ll qx) {
        int id = upper_bound(x.begin(), x.end(), qx) - x.begin();
        --id;
        return li[id](qx);
    }
};

int n;
ll a[maxn],dp,pre[maxn],tot,mx;

struct LINE{
	ll k,b;
	bool operator < (const LINE& ts)const{
		return k>ts.k;
	}
}li[maxn];

void solve(){
	cin>>n;
	rep(i,1,n) cin>>a[i],tot+=i*a[i],pre[i]=pre[i-1]+a[i];
	
	CHT cht;
	cht.init(CHT::line(n,pre[n]));
	
	per(i,n-1,1){
		dp=-cht.query(-a[i])+pre[i]-i*a[i];
		mx=max(mx,dp);
		cht.addLine(CHT::line(i,pre[i]));
	}
	
	cht.init(CHT::line(-1,pre[0]));
	rep(i,2,n){
		dp=-cht.query(a[i])+pre[i-1]-i*a[i];
		mx=max(mx,dp);
		cht.addLine(CHT::line(-i,pre[i-1]));
	}
	
	cout<<mx+tot<<endl;
}

第五题

1388E - Uncle Bogdan and Projections

题意:给 n    ( n < = 2000 ) n\;(n<=2000) n(n<=2000)条平行于 x x x轴的线段,且都位于第一象限,没有重合的线段。选一个方向,使所有线段按这个方向投影到 x x x轴上,使所有端点中,最右边的点    −    \;-\; 最左边的点 最小,求这个最小值。

思路:首先方向可以随便取,有无限种可能,但是我们可以发现,我们选的这个方向必定经过两条线段的两个端点(一条的左端点和另一条的右端点 或 一条的右端点和另一条的左端点),也就是说投影后的线段必定至少有两条是相连的,否则,我们可以将这个方向调整使距离更小。基于这个事实,所有可能的方向就只有 n 2 n^2 n2个,如果对于这 n 2 n^2 n2个方向去计算距离,复杂度是 o ( n 3 ) o(n^3) o(n3)的。
因此我们再考虑如何快速计算 x x x最大和最小的位置,假设当前方向与 x x x负半轴的角度为 a l p h a alpha alpha
请添加图片描述
如图所示,投影后的 x x x坐标可以表示成斜率的形式,因此我们只需要求 m a x 和 m i n max和min maxmin然后 m a x − m i n max-min maxmin即可,即维护两个CHT,一个求 m i n min min,一个求 m a x max max
不过最后代码中还有很多小细节,比如:
1.求方向部分,先枚举两条线段,然后可以求出两个方向,然而在这中间的方向是不能取的(因为投影之后会有重合),因此相当于求区间交,最后拿出区间的两个端点才是有用的方向。
2.CHT部分,求 m i n min min的和求 m a x max max的CHT逻辑是不一样的,不能同时插入线段,并且本题的线段可能有相同的斜率,当有相同的斜率时,应该手动保留 b b b最小的直线。

struct CHT {
    struct line {
        ll k, b;
        line() {}
        line(ll k, ll b): k(k), b(b) {}
        
        double intersect(line l) {
            double db = l.b - b;
            double dk = k - l.k;
            return db / dk;
        }
        
        double operator () (double x) {
            return k * x + b;
        }
    };
    
    vector<double> x;
    vector<line> li;

    void init(line l) {
    	x.clear();
    	li.clear();
        x.push_back(-LINF);
        li.push_back(l);
    }

    void addLine(line l) {
        while (li.size() >= 2 && l.intersect(li[li.size() - 2]) <= x.back()) {
            x.pop_back();
            li.pop_back();
        }
        if (!li.empty()) {
            x.push_back(l.intersect(li.back()));
        }
        li.push_back(l);
    }
    
    double query(double qx) {
        int id = upper_bound(x.begin(), x.end(), qx) - x.begin();
        --id;
        return li[id](qx);
    }
};

struct Vector{
	ll x,y;
	int f;
	double at;
	bool operator < (const Vector& ts) const{ //极角排序
		if(at==ts.at) return f<ts.f;
		return at<ts.at;
//		if(atan2(y,x)==atan2(ts.y,ts.x)) return f<ts.f;
//		return atan2(y,x)<atan2(ts.y,ts.x);
	}
};

vector <Vector> a;

struct node{
	ll l,r,y;
	bool operator < (const node& ts) const{
		if(y==ts.y) return r<ts.r;
		return y>ts.y;
	}
}li[maxn];

int cmp1(node A,node B){
	if(A.y==B.y) return A.l<B.l;
	return A.y>B.y;
}

int cmp2(node A,node B){
	if(A.y==B.y) return A.r>B.r;
	return A.y<B.y;
}


int n;
ll l[maxn],r[maxn],y[maxn];
void solve(){
	cin>>n;
	rep(i,1,n) cin>>l[i]>>r[i]>>y[i],li[i].l=l[i],li[i].r=r[i],li[i].y=y[i];
	rep(i,1,n){
		rep(j,i+1,n){
			if(y[i]==y[j]) continue;
			Vector t;
			t.x=l[i]-r[j];
			t.y=y[i]-y[j];
			t.f=1;
			if(t.y>0) t.x*=-1,t.y*=-1,t.f*=-1;
			t.at=atan2(t.y,t.x);
			a.pb(t);
			
			t.x=r[i]-l[j];
			t.y=y[i]-y[j];
			t.f=-1;
			if(t.y>0) t.x*=-1,t.y*=-1,t.f*=-1;
			t.at=atan2(t.y,t.x);
			a.pb(t);
		}
	}
	
	if((int)a.size()==0) {
		ll mn=LINF,mx=-LINF;
		rep(i,1,n){
			mn=min(mn,l[i]);
			mx=max(mx,r[i]);
		}
		cout<<fixed<<setprecision(10)<<1.0*(mx-mn)<<endl;
		return ;
	}
	
	CHT chtmx,chtmn;
	
	sort(li+1,li+1+n,cmp1);
	chtmn.init(CHT::line(li[1].y,li[1].l));
	rep(i,2,n){
		if(li[i].y!=li[i-1].y){
			chtmn.addLine(CHT::line(li[i].y,li[i].l));
		}
	}
	
	sort(li+1,li+1+n,cmp2);
	chtmx.init(CHT::line(-li[1].y,-li[1].r));
	rep(i,2,n){
		if(li[i].y!=li[i-1].y){
			chtmx.addLine(CHT::line(-li[i].y,-li[i].r));			
		}
	}
	
	sort(a.begin(),a.end());
	
	int now=0;
	double ans=1e30;
	double mn=1e30,mx=-1e30;
	for(auto V:a){
		if(now==0 && V.f==1){
			mn= chtmn.query(-1.0*V.x/V.y);
			mx=-chtmx.query(-1.0*V.x/V.y);
			ans=min(ans,mx-mn);
		} 
		else if(now==1 && V.f==-1){
			mn= chtmn.query(-1.0*V.x/V.y);
			mx=-chtmx.query(-1.0*V.x/V.y);
			ans=min(ans,mx-mn);
		}
		now+=V.f;
	}
	cout<<fixed<<setprecision(10)<<ans<<endl;
}

第六题

932F - Escape Through Leaf

题意:给你一棵以1号点为根的树,一开始有一个人在1号结点,当他在 i i i号结点时,他可以跳到 i i i的子树中的任意一个结点 j j j,花费为 a i ∗ b j a_i*b_j aibj,问最后跳到任意一个叶子结点的最小花费。

d p i dp_i dpi表示 i i i到叶子结点的最小花费,转移为:
d p i = m i n ( d p j + a i ∗ b j )          ( j ∈ s o n i ) dp_i=min(dp_j+a_i*b_j)\;\;\;\;(j\in son_i) dpi=min(dpj+aibj)(jsoni)

bool Q;
struct Line {
    mutable ll k, m, p;
    bool operator<(const Line& o) const {
        return Q ? p < o.p : k < o.k;
    }
};
struct CHT : multiset<Line> {
    const ll inf = LINF;
    ll div(ll a, ll b){
        return a / b - ((a ^ b) < 0 && a % b);
    }
    bool isect(iterator x, iterator y) {
        if (y == end()) { x->p = inf; return false; }
        if (x->k == y->k) x->p = x->m > y->m ? inf : -inf;
        else x->p = div(y->m - x->m, x->k - y->k);
        return x->p >= y->p;
    }
    void add(ll k, ll m) {
        auto z = insert({k, m, 0}), y = z++, x = y;
        while (isect(y, z)) z = erase(z);
        if (x != begin() && isect(--x, y)) isect(x, y = erase(y));
        while ((y = x) != begin() && (--x)->p >= y->p)
            isect(x, erase(y));
    }
    ll query(ll x) {
        assert(!empty());
        Q = 1; auto l = *lower_bound({0,0,x}); Q = 0;
        return l.k * x + l.m;
    }
};

int n;
ll a[maxn],b[maxn],dp[maxn],sz[maxn];
vector <int> G[maxn];

void dfs1(int u,int fa){
	sz[u]=1;
	for(auto v:G[u]) if(v!=fa){
		dfs1(v,u);
		sz[u]+=sz[v];
	}
}

void dfs(int u,int fa,CHT& now){
	
	int leaf=1;
	int mx=0,id;
	
	for(auto v:G[u]) if(v!=fa){
		leaf=0;
		if(sz[v]>mx){
			id=v;
			mx=sz[v];
		}	
	}
	
	if(leaf){
		dp[u]=0;
		now.add(-b[u],-dp[u]);
		return ;
	}
	
	dfs(id,u,now);
	
	for(auto v:G[u]) if(v!=fa && v!=id){
		CHT tmp;
		dfs(v,u,tmp);
		for(auto i:tmp){
			now.add(i.k,i.m);
		}
	}
	
	dp[u]=-now.query(a[u]);
	now.add(-b[u],-dp[u]);
}
void solve(){
	cin>>n;
	rep(i,1,n) cin>>a[i];
	rep(i,1,n) cin>>b[i];
	rep(i,1,n-1){
		int u,v;
		cin>>u>>v;
		G[u].pb(v);
		G[v].pb(u);
	}
	
	dfs1(1,0);
	CHT cht;
	dfs(1,1,cht);
	rep(i,1,n) cout<<dp[i]<<" ";
}

第七题(mark)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值