CF1661D Progressions Covering

题意:

你有两个长度为 n 的数组 a,b。

a数组初始为 0。你每次可以执行一个操作,选定一段长度为 k 的区间

(设区间左端点为 l,则有 1 <=l<=l+k-1<=n ),把从左往右数第i个元素加 i。

给定 n,k 与 b,求当满足 对于任意的 1<=i<=n ,都有a[i]>=b[i] 时 最小的可能操作次数。

1.审题,分析题意。

       理解并翻译一下题意(大家最好自己理解并思考一下):给定b数组及其长度n,选定若干个长度为k的区间,该区间内从左往右数第i个数i(等差数列)(对于b数组本身来说是减去)。求最少操作几次。

2.思考解法及其可行性。

       简单思考一下可以得出一个结论:从右往左看时,如果每个数字还是大于0,说明仍需要继续操作。怎样操作最优呢?对于第 i 个数自然是选取区间 [max(1,i-k+1),i],这样不但能使左边的数字减小,还能使用最小的操作次数就使b[i]减小至0以下。不难推测,这就是绝对最佳的方法。

3.实现思路。

       以刚刚的思路直接写代码(如下图),测一下样例没有问题,但是仔细看会发现时间复杂度为O(nk),看一下数据范围——  1<=k<=n<=3*10^5  ,不进行任何优化的话,TLE无疑。(我第一次就是这样被卡在了第38个测试点)

#include<iostream>
#define int long long
using namespace std;
int n, k;
int b[300300];
signed main() {
	cin >> n >> k;
	int ans = 0;
	for (int i = 1; i <= n; i++) {
		cin >> b[i];
	}
	for(int i=n;i>0;i--){//枚举每一位
		if(b[i]<=0) continue;
		int t=min(k,i);//小坑点
		int now=(b[i]+t-1)/t;//向上取整
		ans+=now;
		int from=max(1ll,i-t+1);
		for(int j=from;j<=i;j++){//对区间进行操作
			b[j]-=(j-from+1)*now;
		}
	}
	cout<<ans;
	return 0;
}

​

既然刚刚的思路正确但是代码有问题,就要重新回到第二步:

2(2).再次思考解法及其可行性。

       既然思路没问题,那么就在算法上进行优化。经过仔细观察与思考,可以发现对于b数组里的每个元素都至少会被访问k遍,这太浪费了!同时每次减去的这个区间里的数字是一个等差数列。而等差数列合并在一起后,其交集还是等差数列。既然是等差数列,那我们就可以用一个新的数组来存储其每个位置之前被剪了多少与这些等差数列的公差,(本人语言表达不太清晰啊,那就看下面这张表)。

                                 

如图这样一个样例,n=3,k=3,b为1 2 3,于是得到这样一串数字。其中红色粗体的便是本算法的关键。所以,我们的新代码思路就此产生——二次差分

(下附代码)

#include<iostream>
#define int long long
using namespace std;
struct c{
	int cha;
	int cnt;
}a[300300];//cnt即之前减掉的部分,cha为减掉部分的公差
int n, k;
int b[300300];
signed main() {
	cin >> n >> k;
	int ans = 0;
	for (int i = 1; i <= n; i++) {
		cin >> b[i];
	}//不言自明
	for(int i=n;i>0;i--){//下方精华!!!!!!
		a[i].cnt-=a[i].cha;//每次剪掉的部分减去公差,就是新的差值
		b[i]-=a[i].cnt;//把b[i]变成实际的模样
		int t=min(i,k);//小坑点,上面有提到
		int now=(b[i]+t-1)/t;//向上取整
		if(b[i]<=0) now=0;//now是等差数列的首项,t是项数。若b[i]<=0,说明不必再继续操作
		ans+=now;//首项即操作次数
		if(i-t-1>0) a[i-t-1].cha-=now;//公差必须要在该等差数列结束后减去!
		a[i-1].cnt+=a[i].cnt+now*t;//更新下一个数值
		a[i-1].cha+=a[i].cha+now;
//		cout<<"cnt:"<<a[i].cnt<<" cha:"<<a[i].cha<<endl;  //调试部分
	}
	cout<<ans;
	return 0;
}

你以为这就完了吗?不,还远远没有。

做一道题,会一类题,更要从题中挖掘出一些值得学习的地方。下面谈谈我的感受——

1.写代码切记不要急躁,也不要因为时间紧迫而心急。心乱了,思路就乱了,代码就写不好了。本人对这点深有感触。

2.多思考,多质疑。我这个思路对吗?有没有特例?这些都要仔细考虑。不然最终很有可能代码写了一大半,时间快没了,才发现思路错误,大把分数付之东流。

3.多使用草稿本,多用实例带进代码里计算一下。使用草稿本不但能增加计算准确率,而且可以记录下思路,避免到后期都不知道自己在写什么。带实例计算可以帮助发现规律,更好地解决题目。

以上是本人的不足,相信也是很多人的问题。希望我们能克服自己的弱点,积极进取,做一个更好的程序员,更优秀的人!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值