[NOI2007] 货币兑换——各种形式的斜率优化DP

题目

[NOI2007] 货币兑换

题解

首先,题目已经提示了,每次要么什么都不干,要么把钱用光,要么把票卖光。

所以很容易想到DP式子:设f[i]表示第 i 天后的最大收益,也就是第 i 天后手头最多有多少钱,而这个钱数的信息又正好可以用来转移后面的DP,所以第 i 天可以什么都不干,即f[i]=f[i-1],也可以用前面某一天赚得的钱来花光买票,即f[i]=max(f[i-1],f[j]/(aj*rj+bj)*rj*ai+f[j]/(aj*rj+bj)*bi),j<i

直接枚举肯定不行,观察式子,发现可以斜率优化,g[j]=f[j]/(aj*rj+bj),考虑枚举中如果 k 比 j 更优,则 g[j]*rj*ai+g[j]*bi<g[k]*rk*ai+g[k]*bi

注意这里 g 不一定单调,因此我们假设 g[j]<g[k],那么有(g[j]*rj-g[k]*rk)/(g[j]-g[k])>-bi/ai

由于 g 和-bi/ai都没有单调性,所以接下来有几种方式进行优化:

CDQ分治

我们仍然考虑左半边对右半边的贡献。

左半边按照 g 排序,于是每次右半边的查询就变成了一个普通的凸包上二分。

总复杂度 O(n log^2 n)

可以精细一点实现,左半边 g 的排序可以从底下归并,右半边可以按照 -bi/ai 归并排序,然后 two pointers 在左边凸包上扫。

这样复杂度就是 O(n log n)

这个做法常数不优,所以就不上代码了。

平衡树维护凸包

斜率优化常规做法:

每次相当于查询凸包上第一段斜率 < -bi/ai 的直线。这个显然可以直接平衡树二分。

计算完之后插入一个点时,暴力删掉两边的不在凸包上的点即可。

总复杂度 O(n log n)

这应该是大部分人的做法,什么treap、splay都属于这类做法,比CDQ快但是不好打。

map代替平衡树

这种做法不推荐,纯粹是笔者懒得自己打平衡树而用STL自带的红黑树(map)取而代之而已,思路也是一样的,只不过要用两个map,一个存斜率一个存坐标,删点时就先新建一个点然后用find函数定位,再通过指针左右扫就行了,(压行后)比平衡树短很多:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<cmath>
#include<vector>
#include<queue>
#include<map>
#define ll long long
#define MAXN 100005
#define INF 0x7f7f7f7f
using namespace std;
inline ll read(){
	ll x=0;bool f=1;char s=getchar();
	while((s<'0'||s>'9')&&s>0){if(s=='-')f^=1;s=getchar();}
	while(s>='0'&&s<='9')x=(x<<1)+(x<<3)+s-'0',s=getchar();
	return f?x:-x;
}
int n,del[MAXN],tp;
double a[MAXN],b[MAXN],r[MAXN],f[MAXN],g[MAXN],S;
map<double,int>kp;
map<double,double>mp;
signed main()
{
	n=read(),S=read();
	for(int i=1;i<=n;i++)scanf("%lf%lf%lf",&a[i],&b[i],&r[i]);
	kp[INF]=0,mp[0]=INF,f[1]=S,kp[r[1]]=1,g[1]=S/(a[1]*r[1]+b[1]),mp[g[1]]=r[1];
	map<double,int>::iterator kt;
	map<double,double>::iterator mt,tt;
	for(int i=2;i<=n;i++){
		double k=-b[i]/a[i],y;
		kp[k],kt=kp.find(k),kt++;
		if(kp[k]==0)kp.erase(k);
		f[i]=max(f[i-1],g[kt->second]*(r[kt->second]*a[i]+b[i]));
		g[i]=f[i]/(r[i]*a[i]+b[i]),tp=0,y=g[i]*r[i];
		if(mp.find(g[i])!=mp.end()){
			int l=kp[mp[g[i]]];
			if(y>g[l]*r[l])kp.erase(mp[g[i]]);
			else continue;
		}mp[g[i]]=y,mt=mp.find(g[i]);
		while(mt!=mp.begin()){mt--;
			double kj=mt->second;int j=kp[kj];
			double ki=(y-g[j]*r[j])/(g[i]-g[j]);
			if(ki>=kj)del[++tp]=j;
			else {mp[g[i]]=ki;break;}
		}mt=mp.find(g[i]),k=mt->second,mt++;
		while(mt!=mp.end()){
			double kj=mt->second;int j=kp[kj];
			double ki=(g[j]*r[j]-y)/(g[j]-g[i]);
			if(kj>=k){del[++tp]=i;break;}
			tt=mt,tt++;
			if(tt==mp.end()||tt->second<ki){
				kp.erase(mt->second),mt->second=ki,kp[ki]=j;break;
			}else del[++tp]=j,mt++;
		}
		for(int j=1;j<=tp;j++){
			if(del[j]==i)mp.erase(g[i]);
			else kp.erase(mp[g[del[j]]]),mp.erase(g[del[j]]);
		}if(mp.find(g[i])!=mp.end())kp[k]=i;
	}
	printf("%.3f\n",f[n]);
	return 0;
}

(究极)李超线段树维护

这个做法就太牛了,我打完map后还在洋洋得意,结果看到这个解法立马自闭。

做法来自洛谷巨佬panyf的博客:https://www.luogu.com.cn/blog/221955/solution-p4027

下面只贴一下大佬的代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+3;
#define db double
db x[N],y[N],a[N],b[N],r[N],c[N],d[N];
int u,s[N*4];
#define f(i,t) (y[t]+x[t]*c[i])
void upd(int k,int t,int l,int r){
    if(l==r){if(f(l,t)>f(l,s[k]))s[k]=t;return;}
    int m=l+r>>1;
    if(f(m,t)>f(m,s[k]))swap(t,s[k]);
    f(l,t)>f(l,s[k])?upd(k*2,t,l,m):upd(k*2+1,t,m+1,r);
}//李超树插入
db qry(int k,int l,int r){
    if(l==r)return f(u,s[k]);
    int m=l+r>>1;
    return max(f(u,s[k]),u>m?qry(k*2+1,m+1,r):qry(k*2,l,m));
}//李超树查询
int main(){
    int n,i;
    db f,g;
    scanf("%d%lf",&n,&f);
    for(i=1;i<=n;++i)scanf("%lf%lf%lf",a+i,b+i,r+i),c[i]=a[i]/b[i],d[i]=c[i];
    sort(c+1,c+n+1);//离散化
    for(i=1;i<=n;++i){
        u=lower_bound(c+1,c+n+1,d[i])-c,f=max(f,b[i]*qry(1,1,n));
        g=a[i]*r[i]+b[i],x[i]=f*r[i]/g,y[i]=f/g,upd(1,i,1,n);
    }
    printf("%.3lf",f);
    return 0;
}

常数小,代码短,又不卡精度,实属最优解!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值