DAY3 (倍增)喷泉问题学习报告

题目如下:
在这里插入图片描述
在这里插入图片描述
你别说,这喷泉长得还真像汉诺塔(bushi)

我记得我第一次考试的时候就有一道汉诺塔,当时我不知道.exe的程序中显示的时间也包括自己输入的时间,结果写了个快速幂(其实用不着)还是超时(太紧张导致手速不够快 ),调了一小时,心态崩溃,最后就得了100/300,也就是只有汉诺塔AC了,真是万恶之源……
怎么我遇到点困难还非得沾点儿汉诺塔(捂脸)

为了考虑在一个位置倒水会到哪儿,需要计算一下该位置到某位置的全部容积,并把他们联系起来。
首先这题有一个单调递增的部分分,假设这些盘单调递增,那么所有的水都是依次向下流,该位置到某位置的容积就是前缀和,此题就完全是一个一般的ST表。(长的完全就是个汉诺塔的样儿)
对于单调递增和一般的喷泉,唯一的区别就是各个盘不都是相连的,应该说是组成很多部分的单调递增序列,所以还需要考虑一下某个盘的下一个盘是哪一个,对这个数组next做个处理就可以了。
基本的指导思想有了,接下来按部就班研究一下各个部分的写法。
(接下来是分析,完整代码和总结在最后)
1.转移方程
先从转移方程开始。在转移的过程中,需要处理两个数组,一个是dp[i][j],表示从i开始向下找2^ j个盘得到的位置;一个是sum[i][j],表示从i开始向下2^ j个盘可取得的总容量(sum不包括自己)。sum在之后用来判断容量是否足够,dp则用来找最终的盘的位置。
尽管这里的dp与一般ST表中的DP不完全一样(或者说dp和sum共同承担了这个功能),但是还是要从二分的思想来。对于一半的位置dp[i][j-1],再向下2^(j-1)步就能得到dp[i][j];sum的处理方式则完全同DP相同,只不过dp[i][j-1]可以代替dp[i][(j-1)<<1]的功能。故总的转移方程如下:

for(j = 1;j <= 17;j++){//2^17>1e5, 2^16<1e5
		for(i = 1;i <= n;i++){
			dp[i][j] = dp[dp[i][j - 1]][j - 1];
			if(dp[i][j]) sum[i][j] = sum[i][j - 1] + sum[dp[i][j - 1]][j - 1];
		}
	}

(初始状态下面再说)
2.next数组和初始化
由于next[i]表示i接下来的第一个盘的位置(注意这个位置与d[i]不直接相关),所以dp[i][0]=next[i],sum[i][0]=c[next[i]]。
为了得出next[i],需要在整个序列中找单调递增区间。
举一个样例的例子(稍作修改):
4 6 3 4 9 4(d[i])
1 2 3 4 5 6(i)
2 5 4 5 0 0 (next[i])
(直接流进水池的,按照答案要求,标记为0)
这个是怎样得出的?我们从队列的方面思考,设一个队列st,对于每一个放进来的d[i],把st[i]与它依次比较,直到找到第一个大于d[i]的st[i],并把d[i]放在st[++i]的位置,这样就可以成功处理单调队列了。next也要基于这个单调队列,next[i]就是第一个大于d[i]的位置i(若没有这么一个位置,就是0)。不过由于i一直在变化,还得用一个数组h存一下i。结合队列的知识,我们就能得到这样的一个完整的初始化代码:

int nxt(int x){
	if(!rear) return 0;
	int l = 1,r = rear,mid;
	while(l < r){
		mid = (l + r + 1) >> 1;
		if(st[mid] > x) l = mid;
		else r = mid - 1;
	}
	if(st[l] > x) return h[l];
	return 0;
}
void before(){
	int i,j;
	for(i = n;i >= 1;i--){
		next[i] = nxt(d[i]);
		while(rear && st[rear] <= d[i]) rear--;
		st[++rear] = d[i],h[rear] = i; 
	}
	for(i = 1;i <= n;i++) dp[i][0] = next[i],sum[i][0] = c[next[i]];
	for(j = 1;j <= 17;j++){
		for(i = 1;i <= n;i++){//注意先j后i,原理见ST表
			dp[i][j] = dp[dp[i][j - 1]][j - 1];
			if(dp[i][j]) sum[i][j] = sum[i][j - 1] + sum[dp[i][j - 1]][j - 1];
		}
	}
}

3.查询
在初始化之前我们显然不知道到底要查什么(不然怎么能叫初始化 ),所以查询一定在初始化之后,结果应该是dp中的某个数值。
对于输入的r、v,指的是从r开始倒v升水,水最后停下的地方,故应该找dp[r][p](p代表未知数)。首先,不要忘了我们的sum数组不包括它本身,因此首先要去掉c[i],把剩下的v放进去。这里查询再次用到了倍增的思想,我们从17开始(为什么是17前面代码有写),依次找小于当前v的最大2^ i,然后去掉这个2^ i,直到i=0为止。这时候,最终的结果就是dp[r][0]。这样的话,就得出下面的代码:(变量并不完全一致)

int solve(int x,int y){
	int i;
	y -= c[x];
	if(y <= 0) return x;
	for(i = 17;i >= 0;i--){
		if(y - sum[x][i] > 0 && dp[x][i]) y -= sum[x][i],x = dp[x][i];
	}//这里判断dp的目的是防止跳出整个喷泉的范围
	return dp[x][0];
}

(注意:尽管在整个dp和sum的循环中都没有考虑到next的存在,但是由于初始状态就是按next[i]赋值,依据昨天二维ST表的思想,我们可以认为dp和sum从一开始就已经混合了next[i],所以不用再去纠结到底这个从i跳2^j步到达的位置水能不能到达。)
结合以上三部分,将函数连接起来,就可以得到最终的完整代码了。完整的代码如下:

#include<cstdio>
using namespace std;
int d[100001],c[100001],dp[100001][18],sum[100001][18],next[100001],st[100001],h[100001];
int rear,n;//再次提醒:数组定义中的数字代表元素个数而不是最大的元素下标
int nxt(int x){
	if(!rear) return 0;
	int l = 1,r = rear,mid;
	while(l < r){
		mid = (l + r + 1) >> 1;
		if(st[mid] > x) l = mid;
		else r = mid - 1;
	}
	if(st[l] > x) return h[l];
	return 0;
}
void before(){
	int i,j;
	for(i = n;i >= 1;i--){
		next[i] = nxt(d[i]);
		while(rear && st[rear] <= d[i]) rear--;
		st[++rear] = d[i],h[rear] = i; 
	}
	for(i = 1;i <= n;i++) dp[i][0] = next[i],sum[i][0] = c[next[i]];
	for(j = 1;j <= 17;j++){
		for(i = 1;i <= n;i++){
			dp[i][j] = dp[dp[i][j - 1]][j - 1];
			if(dp[i][j]) sum[i][j] = sum[i][j - 1] + sum[dp[i][j - 1]][j - 1];
		}
	}
}
int solve(int x,int y){
	int i;
	y -= c[x];
	if(y <= 0) return x;
	for(i = 17;i >= 0;i--){
		if(y - sum[x][i] > 0 && dp[x][i]) y -= sum[x][i],x = dp[x][i];
	}
	return dp[x][0];
}
int main(){
	int m,i,k,v;
	scanf("%d %d",&n,&m);
	for(i = 1;i <= n;i++) scanf("%d %d",&d[i],&c[i]);
	before();
	for(i = 1;i <= m;i++){
		scanf("%d %d",&k,&v);
		printf("%d\n",solve(k,v));
	}
	return 0;
}

总结一下这道题,作为一个倍增,它结合了单调队列的处理和ST表的思想(在我看来还有赋值影响的知识点),而我们得到这个最终的思想都是从一个汉诺塔 已单调递增排列的喷泉开始的。我想这恰恰体现了倍增的优化性,和递推一样,很多复杂的问题都是从0,1,2开始的。
由于这道题的主线思路比较长(dp[i][j]和sum[i][j]的区分是从得结论的过程中得到的,而这两个从最早的状态转移方程就出现了),因此显得思考量很大,很有难度,但是如果能分开解决,会相当程度上进行简化。这同时提醒我们对于这种复杂的问题最好是想好了再开始写,只不过我确实没能养成这个习惯……

最后,这题算是一个DAY3外传,这题我昨天10点开始研究(中间倒腾了一下每日作业),今天才开始写博客,按理说应该先写一下DAY4(DAY4内容量爆炸),但是DAY4DAY5内容关联较大,加上这题再放一放就没意思了,所以尽快补档了。希望今晚写DAY4&DAY5的时候,不至于写到后天(捂脸)

Thank you for reading!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值