天才ACM(倍增)

题目

题目

题解

参考题解:
https://www.acwing.com/solution/content/15458/
很好。

首先考虑我们用贪心证明两个东西:

  1. 如果第 i i i个可以归到前面的部分就归到前面的部分,不要放到后面的部分,反正放到后面也只会让校验值增大,还不如不放。
  2. 对于一个数列而言,如何求校验值?答案是最大的减最小的的平方加上次大的…。
    至于证明,首先,如果对于 ( x − y ) 2 ( x > y ) (x-y)^2(x>y) (xy)2(x>y),而数列中有 没有用的比 x x x更大或比 y y y更小的数字的话,那么我们肯定要用啊。
    第二,对于 a < b < c < d a<b<c<d a<b<c<d而言,最大的方案肯定是: ( a − d ) 2 + ( b − c ) 2 (a-d)^2+(b-c)^2 (ad)2+(bc)2,要证明的话全部化成平方和就变成了问两两数字相乘最小的问题了: a 2 + b 2 + c 2 + d 2 − 2 a d − b c a^2+b^2+c^2+d^2-2ad-bc a2+b2+c2+d22adbc,这个大家肯定都会证明的吧,不会证明看这里:
    在这里插入图片描述
    通过这两个结论就可以证明了。

方法1

因为长度越长校验值越大,根据这个可以二分,也就是二分+check,但是如果每个块都很小的话时间复杂度就很容易被卡成: O ( K n 2 l o g 2 n ) O(Kn^2log^2n) O(Kn2log2n),。

方法2

这个时候就要用到倍增了,倍增算法完美解决了二分处理小数据时无力的问题,倍增的流程是:

  1. 确定一个 x = 1 , R = 1 x=1,R=1 x=1,R=1,然后如果 [ 1 , R + x ] [1,R+x] [1,R+x]可以的话,那么 R + = x , x < < = 1 R+=x,x<<=1 R+=x,x<<=1,并重复1操作,但是如果不可以的话, x > > = 1 x>>=1 x>>=1并跳到2操作。
  2. 如果[1,R+x]可以的话, R + = x R+=x R+=x,不管可不可以, x > > = 1 x>>=1 x>>=1并重复2操作直到 x = 0 x=0 x=0

当然还有一种较慢的倍增:是如果不可以 x > > = 1 x>>=1 x>>=1,可以 x < < = 1 x<<=1 x<<=1,但是会慢(因为其实也是跟上面一样的流程,但是在2步骤中一旦可以会 x < < = 1 x<<=1 x<<=1又要花一次 x > > = 1 x>>=1 x>>=1来重回正轨)。

(其实我一开始的写法是是1操作没有 R + = x R+=x R+=x,跳到2操作时再 R = x R=x R=x,但是没必要,还会慢)

当然,上面的三种做法,单次的时间复杂度都是 O ( log ⁡ t ) O(\log{t}) O(logt) t t t为最终的 R R R),因此倍增是一个时间复杂度很优秀的算法,而且在处理数据块很小时更是展现出比二分大得多的优越性。(至于为什么,你可以把第一个步骤看成找最大 ? ? ?使得 2 ? − 1 ≤ t 2^{?}-1≤t 2?1t,至于第二个步骤,则是又把 ? ? ?遍历一遍,所以是 O ( log ⁡ t ) O(\log{t}) O(logt)

采用快速排序的时间复杂度是 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)

证明我先写的下面的证明,哈哈

设第 i i i个块长度为 l i l_i li,且在倍增第一个步骤中 x x x最大去到了 x x xx xx

注:计算校验值的时间复杂度是块长,而排序的复杂度大于等于块长,因此默认忽略校验值的时间复杂度计算。

第一个步骤是一个 l o g log log,我们不管他,我们要证的是第二个步骤,第一个步骤完毕后,确定的 R R R大于等于 l i 2 \frac{l_i}{2} 2li,因为要check l o g l i logl_i logli次,所以时间复杂度为: O ( R l o g l i l o g R ) = R ( l i l o g 2 l i ) O(Rlogl_ilogR)=R(l_ilog^2l_i) O(RloglilogR)=R(lilog2li)

所有块合并起来的时间复杂度就是: O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)

常数小可以AC此题。

#include<cstdio>
#include<cstring>
#include<algorithm>
#define  N  510000
using  namespace  std;
int  a[N],b[N],n,m;
long  long  T;
inline  int  mymin(int  x,int  y){
    
    return  x<y?x:y;}
bool  check(int  x,int  now)
{
    
    
	int  l=now+1,r=x+now;
	int  t=0;
	for(int  i=l;i<=r;i++)b[++t]=a[i];
	sort(b+1,b+t+1);  
	int  f=t/2;
	long  long  ans=0;
	for(int  i=mymin(f,m);i>=1;i--)ans+=(long  long)(b[t-i+1]-b[i])*(b[t-i+1]-b[i]);
	return  ans<=T;
}
int  main()
{
    
    
	int  K;scanf("%d",&K);
	while(K--)
	{
    
    
		scanf("%d%d%lld",&n,&m,&T);
		for(int  i=1;i<=n;i++)scanf("%d",&a[i]);
		int  now=0,ans=0;
		while(now<n)
		{
    
    
			int  cnt=1,R=1;
			while(check(mymin(R+cnt,n-now),now)==1)
			{
    
    
				R+=cnt;
				if(R>n-now){
    
    R=n-now;break;}
				cnt*=2;
			}
			if(cnt!=n-now)
			{
    
    
				cnt/=2;
				while(cnt)
				{
    
    
					if(check(mymin(R+cnt,n-now),now)==1)
					{
    
    
						R+=cnt;
						if(R>n-now){
    
    R=n-now;break;}
					}
					cnt>>=1;
				}
			}
			now+=R;
			ans++;
		}
		printf("%d\n",ans);
	}
	return  0;
}

方法3

我们上面采用的是快速排序,但是如果用类似归并排序的方法呢?

即我现在已经知道了 X X X,然后要对 X + Y X+Y X+Y进行排序,那么就对 Y Y Y进行排序,然后合并。

这个优化在较快的倍增中的第一个步骤中是毫无优化的,因为: n 2 ∗ log ⁡ n 2 ∗ 2 + n = n log ⁡ n \frac{n}{2}*\log {\frac{n}{2}}*2+n=n\log{n} 2nlog2n2+n=nlogn

但是再第二个步骤中,就不会因为长度较小的 Y Y Y而重复排序了,优化程度很大。

(下面证明用的是目前讲的最快的倍增打法)

那么我们现在就来证明一波时间复杂度:

设第 i i i个块长度为 l i l_i li,且在倍增第一个步骤中 x x x最大去到了 x x xx xx

注:计算校验值的时间复杂度是块长,而排序的复杂度大于等于块长,因此默认忽略校验值的时间复杂度计算。

对于第一个步骤而言: O ( 1 ∗ l o g ( 1 ) + 3 ∗ l o g ( 2 ) + 7 ∗ l o g ( 4 ) + . . . + ( 2 ∗ x x − 1 ) ∗ l o g ( x x ) + ( 4 ∗ x x − 1 ) ∗ l o g ( x x ∗ 2 ) ) = O ( x x log ⁡ x x ) O(1*log(1)+3*log(2)+7*log(4)+...+(2*xx-1)*log(xx)+(4*xx-1)*log(xx*2))=O(xx\log{xx}) O(1log(1)+3log(2)+7log(4)+...+(2xx1)log(xx)+(4xx1)log(xx2))=O(xxlogxx)(根据 a 1 log ⁡ a 1 + a 2 log ⁡ a 2 + . . . + a i log ⁡ a i ≤ s u m log ⁡ s u m a_1\log{a_1}+a_2\log{a_2}+...+a_i\log{a_i}≤sum\log{sum} a1loga1+a2loga2+...+ailogaisumlogsum得出, s u m sum sum为总和)

对于第二个步骤而言:
合并的话,即使假设单次合并的的时间是最终块长的两倍,那么时间复杂度也是: O ( l o g l i l i ) O(log_{l_i}l_i) O(loglili),所以就是分析快速排序的时间复杂度: O ( 1 ∗ l o g ( 1 ) + 2 ∗ l o g ( 2 ) + 4 ∗ l o g ( 4 ) + . . . + x x 2 ∗ l o g ( x x 2 ) = O ( x x log ⁡ x x ) O(1*log(1)+2*log(2)+4*log(4)+...+\frac{xx}{2}*log(\frac{xx}{2})=O(xx\log{xx}) O(1log(1)+2log(2)+4log(4)+...+2xxlog(2xx)=O(xxlogxx),因此对于一个块而言,时间复杂度即为: O ( l i log ⁡ l i ) O(l_i\log{l_i}) O(lilogli),所有块时间复杂度一合并,就是 O ( n l o g n ) O(nlogn) O(nlogn)了。

代码:

#include <iostream>
#include <algorithm>

using namespace std;

typedef long long ll;

const int N = 500005;

int n, m;
int ans;
ll T;
ll w[N], t[N];
ll tmp[N];

ll sq(ll x)
{
    
    
    return x * x;
}

bool check(int l, int mid, int r)           // 判断区间 [l, r) 是否合法,并将 t 中的 [l, mid) 区间和 [mid, r) 区间合并到 tmp 中
{
    
    
    for (int i = mid; i < r; i ++ )         // 将 w 数组的 [l, r) 区间复制到 t 的 [l, r) 区间中
        t[i] = w[i];
    sort(t + mid, t + r);                   // 将 t 的 [mid, r) 排序
    int i = l, j = mid, k = 0;              // 双指针进行区间合并
    while (i != mid && j != r)              // 当 i 不到 mid 且 j 不到 r 时,执行循环
        if (t[i] < t[j])                    // 如果 t[i] 比 t[j] 小,那么将 t[i] 放入 tmp 中
            tmp[k ++ ] = t[i ++ ]; 
        else                                // 否则将 t[j] 放入 tmp 中
            tmp[k ++ ] = t[j ++ ];
    while (i != mid) tmp[k ++ ] = t[i ++ ]; // 处理剩下的元素
    while (j != r) tmp[k ++ ] = t[j ++ ];
    ll sum = 0;                             // 计算校验值
    for (i = 0; i < m && i < k; i ++ , k -- )
        sum += sq(tmp[i] - tmp[k - 1]);
    return sum <= T;                        // 返回当前区间 [l, r) 是否合法
}

int main()
{
    
    
    int K;
    scanf("%d", &K);
    while (K -- )
    {
    
    
        scanf("%d %d %lld\n", &n, &m, &T);
        for (int i = 0; i < n; i ++ )
            scanf("%lld", &w[i]);
        ans = 0;
        int len;
        int start = 0, end = 0;
        while (end < n)
        {
    
    
            len = 1;
            while (len)
            {
    
    
                if (end + len <= n && check(start, end, end + len)) // 如果 w 的 [start, end + len) 区间合法
                {
    
    
                    end += len, len <<= 1;
                    if (end >= n) break ;               // 一个小剪枝,如果 end >= n,那么直接跳出
                    for (int i = start; i < end; i ++ ) // 在 check 时,已经将 t 数组的 [start, end + len) 这段区间归并在 tmp 中了。现在只需要将 tmp 中的有序数组复制到 t 中即可
                        t[i] = tmp[i - start];          // 复制的时候注意下标变换,tmp 是从 0 开始存的,t 是从 start 开始存的
                }
                else    len >>= 1;
            }
            start = end;
            ans ++ ;
        }
        printf("%d\n", ans);
    }
    return 0;
}

作者:垫底抽风
链接:https://www.acwing.com/solution/content/15458/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

猜你喜欢

转载自blog.csdn.net/zhangjianjunab/article/details/107715510
ACM