P9936 [NFLSPC #6] 等差数列

求解思路

1.先把目标写出来

因为要形成一个等差数列,设新形成的等差数列为{bi}

设首项为b1,则bi=b1+(i-1)*d

求需要增加多少满足该序列成为一个公差非正的等差数列

那么增加的数 sum

sum=∑i=1n(b i −a i )\sum_{i=1}^{n} (b~i~-a~i~)∑i=1n(b i −a i )=∑i=1n(b i )\sum_{i=1}^{n} (b~i~)∑i=1n(b i ) ---∑i=1n(a i )\sum_{i=1}^{n} (a~i~)∑i=1n(a i )

∑i=1nb i \sum_{i=1}^{n} b~i~∑i=1nb i =nb1+ n *(n-1)*d/2

2.发现关键点

发现,sum的结果与bi相关,即与公差d与首项b1相关

在这个式子中,有两个未知数,尝试用一个来表示另一个

尝试用公差d表示首项b1

∵ bi≥ai

又∵ bi=b1+(i-1)*d

∴ b1+(i-1)*d≥ai

即b1≥ max(ai - (i-1)*d)

b1是max(ai - (i-1)*d)的最大值

sum=∑i=1n(b i −a i )\sum_{i=1}^{n} (b~i~-a~i~)∑i=1n(b i −a i )

=∑i=1n(b i )\sum_{i=1}^{n} (b~i~)∑i=1n(b i ) ---∑i=1n(a i )\sum_{i=1}^{n} (a~i~)∑i=1n(a i )

=n * max(ai - (i-1)d)+n*(n-1)*d/2-∑i=1n(a i )\sum_{i=1}^{n} (a~i~)∑i=1n(a i )

该式子中包含三项

  • 中间项n*(n-1)*d/2是一个一次函数
  • 尾项 ∑i=1n(a i )\sum_{i=1}^{n} (a~i~)∑i=1n(a i )是常数项
  • 首项=n * max(ai - (i-1)d),其中每一个ai-(i-1)*d是一次函数。

我们暂时不考虑d的范围,即斜率为正还是为负的情况,画出下面的图,即画出多条一次函数,取每一个d点在该图上的最大值

对于每一个d点在各个函数上的最大值,发现他是一个凸函数。

而剩下的一次函数和常数项,不会影响函数的凸凹性。

3.找到解决方法

在凸函数上,斜率非正的部分取最小值,可以用三分求解。

代码实现

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10; 
long long sum,n;
int a[N];
long long cul(long long d)
{
	long long shouxiang=-1e18;
	for(int i=1;i<=n;i++)
	{
		shouxiang=max(shouxiang,a[i]-(i-1)*d);
	}
	return n*shouxiang+n*(n-1)*d/2-sum;
}
int main()
{
	int T;
	cin>>T;
	while(T--)
	{
		cin>>n;
		sum=0;
		for(int i=1;i<=n;i++)  
		{
			cin>>a[i];
			sum+=a[i];
		}
		long long l=-1e9,r=0;
		while(r>l)
		{
			long long ll=l+(r-l)/3;
			long long rr=r-(r-l)/3;
			if(cul(ll)<=cul(rr))
			{
				r=rr-1;
			}
			else l=ll+1;
		}
	   cout<<cul(l)<<endl;
	}
	return 0;
}
 

我的bug

cpp 复制代码
return n*shouxiang+n*(n-1)*d/2-sum;

这里n原本是int 类型,n*(n-1)会爆

三分模板

cpp 复制代码
     l=0.0,r=1000.0;
     while(r-l>eps)
		{
		    k=(r-l)/3;
		    mid1=l+k;
			mid2=r-k; 
			if(f(mid1)>f(mid2))  l=mid1;
			else  r=mid2;
		}
		printf("%.4lf\n",f(l));     

这是我之前写的三分模板

在本题中我刚开始写的三分是

cpp 复制代码
     while(r>l)
		{
			long long k=(r-l)/3;
			long long ll=l+k;
			long long rr=r-k;
			if(cul(ll)>cul(rr))
			{
				l=ll;
			}
			else r=rr;
		}
	   cout<<cul(l)<<endl;

如果 cul(ll) > cul(rr),说明最小值在右边,应该 l = ll + 1

否则最小值在左边,应该 r = rr - 1

这种写法当 ll 和 rr 相邻时,l 和 r 可能不变,造成死循环

改进为:

cpp 复制代码
			if(cul(ll)>cul(rr))
			{
				l=ll+1;
			}
			else r=rr-1;
		

问题 2:循环条件 while(r > l) 太宽松

当 r == l + 1 时,k = (r-l)/3 = 0,那么:

ll = l + 0 = l

rr = r - 0 = r

然后 cul(l) > cul( r) 时 l = l,否则 r = r,区间完全不变 → 死循环。

还可以这么写

cpp 复制代码
         while(r - l > 3)
        {
            long long k = (r - l) / 3;
            long long ll = l + k;
            long long rr = r - k;
            if(cul(ll) > cul(rr))
                l = ll;   // 这里可以不用 +1,因为 ll 和 rr 距离较远
            else
                r = rr;
        }
        // 暴力枚举剩余区间
        long long ans = INF;
        for(long long d = l; d <= r; d++)
            ans = min(ans, cul(d));     
        cout << ans << '\n';
相关推荐
Tisfy34 分钟前
LeetCode 3090.每个字符最多出现两次的最长子字符串:二重循环 / 滑动窗口
算法·leetcode·字符串·题解·模拟·双指针·滑动窗口
-dzk-35 分钟前
【技巧】LC 136.只出现一次的数字
算法·异或
专注仿真1 小时前
问答大模型技术方案算法实现-RAPTOR树构建算法与BEG集成使用
python·算法
zlinear数据采集卡2 小时前
数据采集卡从入门到精通(10):采样率与分辨率的核心关系——反比律、架构分布与过采样
arm开发·嵌入式硬件·算法·fpga开发·架构·开源
GeekZHR3 小时前
C语言指针进阶补充6:动态内存管理、mem系列内存函数、复杂指针声明,一次补齐指针的“三大盲区“
java·c语言·算法·指针
Herbert_hwt3 小时前
第七章 Java深入理解枚举类型
java·开发语言·算法
.道阻且长.4 小时前
8.LeetCode算法习题讲解--滑动窗口--长度最小的子数组
算法·leetcode·职场和发展
(❁´◡`❁)Jimmy(❁´◡`❁)4 小时前
P1156 [USACO01OPEN] 垃圾陷阱
算法·动态规划
疯狂打码的少年4 小时前
【数据结构】二叉排序树(BST)的定义与操作
数据结构·笔记·算法
lally.4 小时前
整数等差数列超图中的三个非微扰现象
算法