Codeforces Global Round 13 C

题目链接
题目大意 对于数组S,每次出发开始可以选择任意元素作为起始点,然后在数组上移动,落点为i + S[i],直至超出数组范围,每次经过的点的值减一(先移动再减/直至减到1为止),求使数组元素全为1所用最少的出发次数

数据范围 数组大小n:1<=n<=5000 元素大小:1<=a[i]<=1e9

思路 首先考虑到数组从前向后的第一个非1元素,这类元素如果不是从他开始那么他将永远不会减一,因此每一次弹跳的起始点选择都应该是第一个非1元素,然后只需要模拟弹跳过程,同时记录数组中1的个数,当1的个数和数组大小相同时,说明数组里面已经全是1了,那么就只要输出此时的次数就行了。

但是这么做,显而易见的,就t了,因为如果每次跳过的距离很短,而且数组还开的很大的话,执行的次数就会过多了。

而优化的方法就考虑贪心的做法,尽量的减少没必要的跳跃过程,如当第一个非1元素保存的S[i]过于大,导致i+S[i]的结果超出了n,那么就可以将i+S[i]-n次的超界操作省略,直接修改S[i]的大小并改变cnt(用于保存出发了几次)的大小。另外如出发后经过S[i]值为1的位置时,我们可以直接跳到下一个非1元素,因为经过1的时候每次都是一步一步走,一定不会漏掉任何一个元素,而且1也不会改变,所以直接跳过是没有问题的。

修改的过程中也出现了一点问题,就是我构造nxt数组来存储处于i位置的元素的next指针,而我考虑了好一会怎么维护1元素的next指针,即指向下一个非1元素,我觉得每次维护都用遍历的话可能会有n2的复杂度,会不会还是t,后来想想完全是多虑,数组长度总共只有5000,就算是n2,也就2e6完全不用担心t。

#include<iostream>
#include<cstdio>
using namespace std;
int t,n,numOne,firNoOne,pos,lastNoOne,lastOne;
long long S[5005];
long long cnt;

struct node//用于记录每个非一结点的前一个非一结点和后一个非一结点的序号
{
	int last = 0;
	int next = 0;
}nxt[5005];

int main()
{
	scanf("%d",&t);
	while(t--)
	{
		cnt = 0;
		numOne = 0;
		firNoOne = 0;
		scanf("%d",&n);
		for(int i = 1;i <= n;i++)
		{
			scanf("%lld",&S[i]);
			if(S[i]==1)//在输入的时候就先记录1出现过的次数
				numOne++;
			else if(S[i]!=1&&firNoOne == 0)//第一个非一结点
			{
				firNoOne = i;
				lastNoOne = i;
			}else if(S[i]!=1)//后续非一结点
			{
				nxt[lastNoOne].next = i;
				nxt[i].last = lastNoOne;
				lastNoOne = i;
			}
		}
		nxt[lastNoOne].next = n+1;//最后一个非一结点的下一个一个非一结点记为n+1 方便查到的时候直接跳出
		lastOne = n+1;
		for(int i = n;i >= 1;i--)//更新所有一结点的next
		{
			if(S[i]!=1) lastOne = i;
			else nxt[i].next = lastOne;
		}
		

		while(numOne!=n)//开始模拟
		{
			if(S[firNoOne]==1)//先判定此时第一个非一标记标记的位置的S值是否已经到1
			{
				firNoOne = nxt[firNoOne].next;
			}
			pos = firNoOne;
			if(S[pos]+pos > n&&pos != n)//特殊情况 第一个弹床就直接把人弹出去 直接减掉 少进几次循环 减到弹到最后一张蹦床的情况 
			{
				cnt += S[pos] + pos - n;
				S[pos] = n - pos;
				if(S[pos]==1) numOne++;//特殊情况 倒数第二的时候如果直接减掉 S会只剩下1 这个时候numOne也要加一下
				continue;
			}
			//但是还有一种更特殊的情况 就是从最后一个蹦床开始跳 无论S是几他都会跳出去 而且最后加的cnt会多加1 因为他不能从自己跳到自己 那就再多写一个特殊情况
			if(pos == n)//而且这种情况也没必要进入下面的while循环 因为他每次就跳一下 那就直接贪心
			{
				cnt += S[pos]-1;
				numOne++;
				continue;
			}
			
			cnt++;
			while(pos <= n)//进入跳一跳环节
			{
				if(S[pos]!=1) lastNoOne = pos;//跳到非一结点 标记一下 因为如果再下一次跳到的是一结点 就可以直接飞到下一个非一结点
				else if(S[pos] == 1)//跳之前就是1的话就直接飞到下一个非一
				{
					pos = nxt[pos].next;
					continue;
				}
				S[pos]--;
				if(S[pos] == 1)//跳完之后变成1的话 需要更新一下nxt数组 
				{
					numOne++;
					if(pos!=firNoOne)
					{
						nxt[nxt[pos].last].next = nxt[pos].next;
						if(nxt[pos].next!=n+1)	//如果是最后一个非一结点 就不需要更新后一个结点的last 也不需要更新next
						{
							nxt[nxt[pos].next].last = nxt[pos].last;
							for(int j = pos;S[j]==1;j--)
							{
								nxt[j].next = (S[j+1] == 1)? nxt[j+1].next:j+1;
							}
						}
					}
				}
				pos = pos+S[pos]+1;
			}
		}

		printf("%lld\n",cnt);
	}
	return 0;
}

第一次写博客题解,算是记录一下我acm的开始,之后打了训练赛之后的题解也尽量保持写博客的习惯,望坚持。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值