【题解】XM捡面筋

\(XM\) 捡面筋

————感谢XMZ贡献的名誉题

题目背景

众所周知,\(XM\) 是一个爱吃面筋的孩子。

题目描述

lcez的面筋生产部有\(n\)栋楼排成一列,每栋楼有\(h_i\)层(从第\(1\)层开始算,第\(h_i\)层是楼顶),第\(i\)栋楼的每层都有价值为\(v_i\)的面筋。而\(XM\) 就在楼之间捡面筋。

\(XM\) 可以花费一单位时间向上或向下一层。当然,为了展示他高超的跳楼技巧,他可以从一个楼的某一层跳到相邻楼的同层,如果相邻楼没有当前楼高,\(XM\) 就会跳到相邻楼的楼顶。他在相邻两栋楼之间跳跃一次花费的时间也是一个单位时间。

特别地,因为每栋楼的楼顶没有围栏,当\(XM\) 在楼顶时,爱装*的\(XM\) 可以一下子跳到本栋楼的一楼,而只花费一个单位时间。\(XM\) 捡面筋不需要花费时间。

\(XM\) 现在站在第1栋楼的楼顶津津有味地吃着楼顶的面筋,问题是还有\(m\)个单位时间\(XM\) 就要回去打\(WZ\)了,他想知道自己最多能得到多少价值的面筋。

输入格式

第一行两个正整数\(n\)\(m\)

下面\(n\)行每行两个正整数\(h_i\)\(v_i\)

输出格式

一个正整数,表示\(XM\) 能获得的面筋价值最大值

样例输入

3 3
2 1
1 5
3 4

样例输出

14

数据规模及约定

//喂喂喂,别忘了开long long

对于\(10\)%的数据,\(v_i\) = 1 (聪明的你就知道该输出什么了

对于另外\(10\)%的数据,n = 10

对于另外\(20\)%的数据,\(n <= 100\)

对于另外\(20\)%的数据,\(n<=1000\)

对于另外\(20\)%的数据,\(n <= 10000\)

对于\(100\)%的数据,\(n,m <= 100000\) , \(h_i,v_i <= 2000\)

题解

问题的实质

1)环

每栋楼其实是一个环。不管是从哪一层到达当前楼,花费多长时间,就能得到多少面筋。(忽略从旁边楼过来的时间,我们只看在楼内部花费的时间)。也就是说楼可以任意分割。

知道每栋楼是一个环,这很重要。

2)简单背包

那么这其实是一个背包问题,并且由于楼可以分割,这个问题就变成了最简单的贪心买东西问题:

按性价比由高到低排序的背包问题。

不理解吗?就是这个

求解并得分

而且我的数据好像不用开\(longlong\)也行QAQ

在楼间跳跃,跳到下一栋再跳回来?聪明的\(XM\)肯定不会这么做。

我们不确定最后一单位时间\(XM\)在哪一栋楼,所以最外层枚举结束的楼。

确定了最后一栋楼是第\(i\)栋,现在我们要做的就是将前\(i\)栋楼按\(v\)排序,然后在\(m\)允许范围内把\(v\)最大的整栋楼的面筋捡光。当剩余时间不允许捡光整栋楼的时候,能捡多少捡多少(分割物品)。

具体实现时,可以用\(sort\)实现前\(i\)栋楼的有序排列,也可以用priority_queue​

需要注意的是,\(XM\)是在第\(1 - i\)栋楼之间连续跳跃,所以这\(i\)栋楼每一栋都要至少花费1个单位时间(不管他的v是多少)(第一栋楼不需要花费1单位时间)。

对于循环的每一个\(i\),我们得到一个\(ans_i\),最后的\(ans = max(ans_i)\)

code#1

复杂度分析:\(O(n^2 log n)\),只能应对\(1000\)以内的数据

#include<cmath>
#include<queue>
#include<cstdio>
#include<string>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 100010;
int n, m, tim, usedti;
long long ans, ans0;
struct build{
	int h, v, id;
}bd[maxn]; 
bool operator < (build a, build b){
	return a.v < b.v;
}
priority_queue<build> q;
/*
bool cmp(build a, build b){
	return a.v > b.v;
}*/
int main(){
	freopen("gluten10.in","r",stdin);
	freopen("gluten10.out","w",stdout);
	scanf("%d%d",&n,&m);
	for(int i = 1; i <= n; i++){
		scanf("%d%d",&bd[i].h,&bd[i].v);
		bd[i].id = i;
		bd[i].h --;
	}
	for(int end = 1; end <= n; end++){
		for(int i = 1; i <= end; i++)	q.push(bd[i]);
		ans0 = 0;
		tim = m - end + 1;
		usedti = 0;
		for(int i = 1; i <= end; i++)		ans0 += bd[i].v;
//		sort(bd + 1, bd + 1 + end, cmp);
		for(int i = 1; i <= end; i++){
			int id = q.top().id;
			q.pop();
			if(usedti + bd[id].h <= tim){
				ans0 += bd[id].h * bd[id].v;
				usedti += bd[id].h; 
			}		
			else{
				ans0 += bd[id].v * (tim - usedti);
				usedti = tim;
			} 
		}
//		cout << end << " " << ans0 << endl;
		ans = max(ans, ans0); 
	}
	printf("%d\n",ans);
	return 0;
}

正解

上述代码之所以是\(n^2logn\)级别,是因为对于每一次循环,我们都用\(O(n)\)的复杂度将\(1-end\)入队,再用\(O(n)\)计算每一个物品的价值贡献。

正解是这样的:

维护一个小根堆\(q\),堆内元素是楼(结构体build),维护堆内所有楼的面筋总价值和sumv,维护堆内所有楼的层数和sumh(也就是捡完全部楼所要花费的时间)。

每次循环都将当前这栋楼扔小根堆。当\(sumh\) 大于我们能够自由支配的时间(设为\(m0\))时,从小根堆里不断地弹出一些\(v\)值很小的楼。弹出的时候\(sumh\)\(sumv\)随着减小。如果弹出当前堆内\(v\)最小的楼之后\(sumh\) 要小于我们能自由支配的时间\(m0\),停止弹出,此时\(sumh\)刚好大于\(m0\)

然后计算价值。注意到计算价值时有两种情况。

1)如果一开始\(sumh\)就小于\(m0\),也就是说没有从对内弹出任何元素,那么答案就是\(sumv\)加上经过所有楼一次的时间。

2)否则。当前队内的第一个元素(也就是\(v\)最小的那栋楼)我们分割它,其余队内的楼我们全捡一遍。

code#2

#include<cstdio>
#include<cstdlib>
#include<map>
#include<cmath>
#include<ctime>
#include<queue>
#include<string>
#include<cstring>
#include<iostream>
#include<algorithm>
#define ll long long
#define ci cosnt int &
using namespace std;
const int maxn = 1000010;
int n, m, h, v;
ll ans, passgot, sumv, sumh;
struct build{
	int h, v;
	build(){}
	build(int h0, int v0) : h(h0), v(v0){};
};
bool operator < (build a, build b){
	return a.v > b.v;
}
priority_queue <build> q; 
int main(){

	scanf("%d%d",&n,&m);
	m++;
	for(int i = 1; i <= n; i++){
		scanf("%d%d",&h, &v);
		m--;
		passgot += v;
		q.push(build(h-1, v));
		sumh += h-1;
		sumv += 1ll*v*(h-1);		
		while(q.size() && sumh - q.top().h >= m){
			sumh -= q.top().h;
			
			sumv -= q.top().v * q.top().h * 1ll;
			q.pop();
		}
/*		if(sumh >= m){
			ans = max(ans, sumv - (sumh - m) * q.top().v + passgot);
			cout << "*" << sumv << endl;
		}
		else ans = sumv + passgot;
		cout << ans << endl;
*/
		ans = max(ans, sumh >= m ? sumv - (sumh - m) * q.top().v + passgot : sumv + passgot);
		if(m <= 0)	break;
	}
	printf("%lld\n",ans);
	return 0;
}

code#3

事实上,手写堆的常数比STL堆的常数小很多,可以跑\(10^6\)的数据

#include<bits/stdc++.h>
const int S=1<<20;
char frd[S],*ihead=frd+S;
const char *itail=ihead;

inline char nxtChar()
{
	if(ihead==itail)
	    fread(frd,1,S,stdin),ihead=frd;
	return *ihead++;
}

template<class T>
inline void read(T&res)
{
	char ch;
	while(ch=nxtChar(),! isdigit(ch));
	res=ch^48;
	while(ch=nxtChar(),isdigit(ch))
	    res=res*10+ch-48;
}

typedef long long ll;
const int N=1e6+5;
int n;
ll m,tot_h,tot_v,ans;
template<class T>
inline void CkMax(T & x,T y){if(x<y) x=y;}

struct point
{
	int v,h;
	point(){}
	point(int V,int H):
	    v(V),h(H){}
	
	inline bool operator<(const point & a)const
	{
		return v<a.v;
	}
};

struct Heap
{
	point g[N];
	int len;
	
	inline void Push(const point & res)
	{
		int now=++len,nxt=len>>1;
		while(nxt)
		{
			if(res<g[nxt])
			    g[now]=g[nxt],now=nxt,nxt>>=1;
			else break;
		}
		g[now]=res;
	}
	inline void Pop()
	{
		int now=1,nxt=2;
		point res=g[len--];
		while(nxt<=len)
		{
			if(nxt<len&&g[nxt|1]<g[nxt]) nxt|= 1;
			if(g[nxt]<res)
			    g[now]=g[nxt],now=nxt,nxt<<=1;
			else break;
		}
		g[now]=res;
	}
}Q;

int main()
{
	read(n);read(m);++m;
	ll res=0;
	for(int i=1,h,v;i<=n;++i)
	{
		read(h);read(v);
		--m;
		res+=v;
		if(!m) break;
		if(h>1)
		{
			point tmp=point(v,h-1);
			Q.Push(tmp);
			tot_h+=tmp.h;
			tot_v+=1ll*tmp.h*tmp.v;
			while(Q.len&&tot_h-Q.g[1].h>=m)
			{
				tot_h-=Q.g[1].h;
				tot_v-=1ll*Q.g[1].h*Q.g[1].v;
				Q.Pop();
			}
		}
	    CkMax(ans,tot_h>=m?
	        res+tot_v-1ll*Q.g[1].h*Q.g[1].v+1ll*(m-tot_h+Q.g[1].h)*Q.g[1].v:res+tot_v);
	}
	std::cout<<ans<<std::endl;
	return 0;
}

本地测,当\(n = 1e6, m = 1e6, h_i <= 1000, v_i <= 1000\)时,手写堆跑了\(147ms\),STL的优先队列跑了\(1433ms\),大概是\(10\)倍的差距。所以建议学一下手写堆。

Thanks

posted @ 2020-07-16 21:45  _Buffett  阅读(243)  评论(4编辑  收藏  举报