「NOI2014」购票 数据结构+斜率优化

「NOI2014」购票

Part 0

测试点1,2,3。枚举,复杂度 O ( n 2 ) O(n^2)

Part 1

对于t=0的测试点。

我们看一看这个状态转移式子。

记dp[i]为i这个点到达SZ市所需的最小资金dis[i]为SZ市到这个点i的路径长度。我们要求t这个点的答案,那么就有。(s为t到根路径上的点)
d p [ t ] = min ( d p [ s ] + p [ t ] ( d i s [ t ] d i s [ s ] ) + q [ t ] ) dp[t]=\min(dp[s]+p[t]*(dis[t]-dis[s])+q[t])
看到这个式子,不妨尝试一下斜率优化,于是将式子转化。
d p [ t ] = min ( d p [ s ] p [ t ] d i s [ s ] ) + q [ t ] + p [ t ] d i s [ t ] dp[t]=\min(dp[s]-p[t]*dis[s])+q[t]+p[t]*dis[t]
然后按照斜率优化的套路
y = d p [ s ] x = d i s [ s ] k = p [ t ] b = y k x 令y=dp[s]\\ x=dis[s]\\ k=p[t]\\ 再令b=y-kx
那么我们要求的就是 d p [ t ] = min ( b ) + q [ t ] + p [ t ] d i s [ t ] dp[t]=\min(b)+q[t]+p[t]*dis[t]

对于点t到根到路径上的点x和y。如果点x比点y优,那么 d p [ x ] p [ t ] d i s [ x ] < d p [ y ] p [ t ] d i s [ y ] dp[x]-p[t]*dis[x]<dp[y]-p[t]*dis[y] ,也就是
d p [ x ] d p [ y ] d i s [ x ] d i s [ y ] < p [ t ] \frac{dp[x]-dp[y]}{dis[x]-dis[y]}<p[t]
这就可以看成两个点构成直线的斜率。

于是我们维护一个斜率单调递增的单调栈(也就是下凸包)。

这一步的跳跃如果看不懂有三种解决方案。1手动模拟。2参考一下这篇题解的第二个性质,第一个性质由于K不具有单调性故不存在。3模拟一下上凸包,可以发现中间那些点不可能最优。

因为P[t]不具有单调性(深度大的点的P不一定比深度小的点的P大)故我们每次查询答案要在单调栈上二分。由于我们维护的点的斜率递增,那么我们拿每个点与前面的点比较,如果他们之间的斜率大于p[t],那么之后的斜率也大于p[t],此时后面的点都没有前面的更优。故我们只用二分最后一个比前面的更优的点。(注意一下二分的边界)

这个部分分的代码:

#include<cstdio>
#define M 200005
int fa[M];
long long Q[M],L[M],dp[M],D[M],P[M];
long long dis[M];
struct node{long long x,y;}stk[M];
int top;
bool cmp(node A,node B,long long K){//B比A优 
	return 1.0*K*(B.x-A.x)>B.y-A.y;
}
int Get_mx(long long K){
	int L=2,R=top,res=1;//注意二分边界 
	while(L<=R){
		int mid=(L+R)>>1;
		if(cmp(stk[mid-1],stk[mid],K)){
			L=mid+1;
			res=mid;
		}else R=mid-1;
	}
	return res;
}
bool check(node A,node B,node C){//是否A--C的斜率小于A--B的斜率 
	return 1.0*(C.y-A.y)*(B.x-A.x)<1.0*(B.y-A.y)*(C.x-A.x);
}
int main(){
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=2;i<=n;i++)scanf("%d%lld%lld%lld%lld",&fa[i],&D[i],&P[i],&Q[i],&L[i]);
	stk[++top]=(node){0,0};
	for(int i=2;i<=n;i++){
		dis[i]=dis[i-1]+D[i];//dis为这个点到SZ市的距离 
		int p=Get_mx(P[i]);//二分最优点 
		dp[i]=stk[p].y-P[i]*stk[p].x+Q[i]+P[i]*dis[i];//计算答案 
		node New=(node){dis[i],dp[i]};
		while(top>1&&check(stk[top-1],stk[top],New))top--;//维护下凸包 
		stk[++top]=New;
		printf("%lld\n",dp[i]);
	}
	return 0;
}

Part 1

对于t=1

我们要一直维护一个从根结点到这个点的单调栈。故我们每次弹出都不能是真的弹出,因为之后还要回溯。对于一个单调栈,我们可以用一个可以回溯的栈。在每次插入一个元素时,我们可以找到这个元素应该插入到什么位置,我们存下当时的top。然后把这个位置的元素放到栈的最后面,然后把top指向这个位置。然后撤销操作就是把这个位置的元素放到当前top的位置,再把top指向之前的位置即可。

const int SIGN=-2019;//直接插入的标志 
struct Stack{
	int top;
	vector<node>stk;
	stack<int>used;//存上一个top的位置 
	void clear(){
		top=-1;//初始top为0(个人习惯) 
		stk.clear();while(!used.empty())used.pop();
	}
	void Push(node New){//0~top的元素才算是stk内的元素,后面的是为了撤销用的(本来是要在开一个栈的,但是懒QWQ) 
		int L=1,R=top,res=SIGN;
		while(L<=R){//找到它应该插入的位置,就是之前的一直弹到的位置 
			int mid=(L+R)>>1;
			if(check(stk[mid-1],stk[mid],New)){
				res=mid;
				R=mid-1;
			}else L=mid+1;
		}
		if(res==SIGN){//如果可以之间插在后面,但这个时候不可以直接插,别忘了top后面还有元素 
			stk.push_back(stk[++top]);//把这个元素加到栈(但不是top的后面)的最后面 
			stk[top]=New;
			used.push(SIGN);
		}else {
			int tmp=top;
			stk.push_back(stk[res]);
			stk[res]=New;
			top=res;
			used.push(tmp);
		}
	}
	void Backtrace(){
		int Lst=used.top();used.pop();//上一个top 
		stk[top]=stk[stk.size()-1];stk.pop_back();//把这个元素回到它原来的位置 
		if(Lst==SIGN)top--;//如果上一步是直接插入 
		else top=Lst;//top重置 
	}
	long long Query(long long K){
		if(top<0)return 1e18;
		int L=1,R=top,res=0;
		while(L<=R){//同Part0的Get_mx 
			int mid=(L+R)>>1;
			if(cmp(stk[mid-1],stk[mid],K)){
				L=mid+1;
				res=mid;
			}else R=mid-1;
		}
		return Calc(stk[res],K);
	}
}S;

总代码如下:

扫描二维码关注公众号,回复: 5939623 查看本文章
#include<cstdio>
#include<vector>
#include<stack>
#define M 200005
using namespace std;
struct E{
	int to,nx;
}edge[M<<1];
int tot,head[M];
void Addedge(int a,int b){
	edge[++tot].to=b;
	edge[tot].nx=head[a];
	head[a]=tot;
}
int fa[M];
long long Q[M],L[M],dp[M],D[M],P[M];
long long dis[M];
struct node{
	long long x,y;
}stk[M];
bool cmp(node A,node B,long long K){//B比A更优 
	return 1.0*K*(B.x-A.x)>=B.y-A.y;
}
bool check(node A,node B,node C){
	return 1.0*(C.y-A.y)*(B.x-A.x)<=1.0*(B.y-A.y)*(C.x-A.x);
}
long long Calc(node A,long long K){//算截距 
	return A.y-A.x*K;
}
struct Stack{···}
void dfs(int now){
	dis[now]=dis[fa[now]]+D[now]; 
	if(now!=1)dp[now]=S.Query(P[now])+Q[now]+P[now]*dis[now];//算答案 
	node New=(node){dis[now],dp[now]};
	S.Push(New);
	for(int i=head[now];i;i=edge[i].nx){
		int nxt=edge[i].to;
		dfs(nxt);
	}
	S.Backtrace();
}
int main(){
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=2;i<=n;i++){
		scanf("%d%lld%lld%lld%lld",&fa[i],&D[i],&P[i],&Q[i],&L[i]);
		Addedge(fa[i],i);
	}
	S.clear();
	dfs(1);
	for(int i=2;i<=n;i++)printf("%lld\n",dp[i]);
	return 0;
}

Part 2

正解

有了L这个约束之后,我们就需要知道从根到这个点的dep大于L的元素所构成的单调栈(同样要支持撤销)。我们可以维护一个个小区间的元素然后分别计算答案,取最小值即为一个大区间的答案。区间问题有很多种解法,像线段树,树状数组。这里用树状数组维护一个后缀。每一个树状数组的节点维护的就是他所管辖的元素所构成的单调栈(当然加入时是按dep加入的)。

代码实现如下:

struct Bin{
	Stack S[M];
	void Build(){for(int i=1;i<=n;i++)S[i].clear();}
	void Updata(int x,node New){
		while(x){
			S[x].Push(New);
			x-=lowbit(x);
		}
	}
	void Backtrace(int x){//回撤
		while(x){
			S[x].Backtrace();
			x-=lowbit(x);
		}
	}
	long long Query(int x,long long K){
		long long res=1e18;
		while(x<=n){//答案就是每个小的区间的最小值
			res=min(S[x].Query(K),res);
			x+=lowbit(x);
		}
		return res;
	}
}B;

那么正解的代码就是这样:

#include<cstdio>
#include<stack> 
#include<vector>
#define M 200005
#define lowbit(x) x&-x
using namespace std;
struct E{
	int to,nx;
}edge[M<<1];
int tot,head[M];
void Addedge(int a,int b){
	edge[++tot].to=b;
	edge[tot].nx=head[a];
	head[a]=tot;
}
int n,m;
int fa[M];
long long Q[M],Len[M],dp[M],D[M],P[M];
long long dis[M];
struct node{
	long long x,y;
};
int top;
bool cmp(node A,node B,long long K){//B比A优 
	return 1.0*K*(B.x-A.x)>B.y-A.y;
}
bool check(node A,node B,node C){
	return 1.0*(C.y-A.y)*(B.x-A.x)<1.0*(B.y-A.y)*(C.x-A.x);
}
long long Calc(node A,long long K){//算截距 
	return A.y-A.x*K;
}
const int SIGN=-2019;//直接插入的标志 
struct Stack{···};
struct Bin{···}B;
int FindL(int R,long long len){//取这个深度区间的最优左端点 
	int L=1,ans=R+1;
	while(L<=R){
		int mid=(L+R)>>1;
		if(dis[mid]>=len){
			ans=mid;
			R=mid-1;
		}else L=mid+1;
	}
	return ans;
}
void dfs(int now,int dep){
	dis[dep]=dis[dep-1]+D[now];//这里的dis[i]对于表示now到根结点路径上的点,dep为i的点到根结点的距离 
	if(now!=1){
		int Lx=FindL(dep,dis[dep]-Len[now]);
		dp[now]=B.Query(Lx,P[now])+Q[now]+P[now]*dis[dep];
	}
	node New=(node){dis[dep],dp[now]};
	if(head[now])B.Updata(dep,New);//叶子节点不用加 
	for(int i=head[now];i;i=edge[i].nx){
		int nxt=edge[i].to;
		dfs(nxt,dep+1);
	}
	if(head[now])B.Backtrace(dep);
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=2;i<=n;i++){
		scanf("%d%lld%lld%lld%lld",&fa[i],&D[i],&P[i],&Q[i],&Len[i]);
		Addedge(fa[i],i);
	}
	B.Build();
	dfs(1,1);
	for(int i=2;i<=n;i++)printf("%lld\n",dp[i]);
	return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_35320178/article/details/89322189