0x06倍增

倍增,字面意思就是“成倍增长”。这是指我们在进行递推时,如果状态空间很大,通常的线性递推无法满足时间与空间复杂度的要求,那么我们可以通过成倍增长的方式,只递推状态空间中在2的整数次幂位置上的值作为代表。当需要其他位置的值时,我们通过“任意整数可以表示成若干个2的次幂项的和”这一性质,使用之前求出的代表值拼成所需的值。所以使用倍增算法也要求我们递推的问题的状态空间关于2的次幂具有可划分性。
试想这样一个问题:给定一个长度为N的数列A,然后进行若干次询问,每次给定一个整数T,求出最大的K,满足A[I]+A[2]+…+A[K]<=T。你的算法必须是在线的。
最朴素的做法当然是从前向后枚举K,每次询问花费的时间与答案的大小有关,最坏情况下o(N);
如果我们能够先花费O(N)的时间预处理A数组的前缀和数组S,就可以二分K的位置,比较S[k]与T的大小来确定二分上下界的变化,每次询问花费的时间都是O(logN)。这个算法在平均情况下表现很好,但是它的缺点是如果每次询问给定的整数T都非常小,造成答案K也非常小,那么该算法可能还不如从前向后枚举更优。
我们可以设计这样一种倍增算法
1.令p=1,k=0,sum=0;
2.比较"A数组中k之后的p个数的和"与T的关系,也就是说,如果sum+S[k+p]-S[k]<=T,则令sum+=S[k+p]-S[k],k+=p,p*=2,即累加上这p个数的和,然后把p的跨度增长一倍。如果sum+S[k+p]-S[k]>T,则令p/=2。
3.重复上一步,直到p的值变为0,此时k就是答案。

题目一:天才ACM
给定一个整数 M,对于任意一个整数集合 S,定义“校验值”如下:
从集合 S 中取出 M 对数(即 2∗M 个数,不能重复使用集合中的数,如果 S 中的整数不够 M 对,则取到不能取为止),使得“每对数的差的平方”之和最大,这个最大值就称为集合 S 的“校验值”。
现在给定一个长度为 N 的数列 A 以及一个整数 T。
我们要把 A 分成若干段,使得每一段的“校验值”都不超过 T。
求最少需要分成几段。
输入格式
第一行输入整数 K,代表有 K 组测试数据。
对于每组测试数据,第一行包含三个整数 N,M,T 。
第二行包含 N 个整数,表示数列A1,A2…AN。
输出格式
对于每组测试数据,输出其答案,每个答案占一行。
数据范围
1≤K≤12,
1≤N,M≤500000,
0≤T≤1018,
0≤Ai≤220
输入样例:
2
5 1 49
8 2 1 7 9
5 1 64
8 2 1 7 9
输出样例:
2
1

题目思路:
首先,对于一个集合S,显然应该取S中最大M个数和最小的M个数,最大和最小构成一对,次大和次小构成一对,这样求出的“校验值”最大。而为了让数列A分成的段最少,每一段都应该在“校验值”不超过T的前提下,尽量包含更多的数。所以我们从头开始对A进行分段,让每一段尽量长,到达结尾时分成的段数就是答案。
于是,需要解决的问题为:当确定一个左端点L之后,右端点R在满足A[L]到A[R]的“校验值”不超过T的前下,最大能取到多少。假设我们二分取端点R,如果T的值比较小,最终的右端点R可以只扩展了一点儿。浪费了很多时间。所以我们可以使用倍增算法。
1.初始化p=1,R=L;
2.求出[L,R+p]这一段区间的“校验值”,若“校验值”<=T,则R+=p,p*=2,否则p/=2;
3.重复上一步,直到p的值变为0,此时R即为所示。
上面这过程至多循环O(logN)次,每次循环对长为O(R-L)的一段进行排序,完成整个题目的求解累计扩展长度为N,所以总体时间复杂度为O(Nlog^2N)。实际上我们每次求“校验值”时可以不用快速排序,而是采用类似归并排序的方法,只对新增的长度部分排序,然后合并新旧两段,这样总体时间复杂度可以降低到O(NlogN)。

下述代码中,在处理 [start,end) 的时候,已经将 [start,end)排好序了,所以不需要在处理 [start,end+len)时再排序。
处理 [start,end+len)时,只需要将 [end,end+len) 排序,然后将 [start,end) 与 [end,end+len) 这两段区间进行归并即可。

由于区间 [start,end) 是左闭右开的,直接让 start=end,ans++ 即可

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 500010;
long long w[N],t[N],temp[N];  //数组w用来存储输入的数    数组t用来存储排序后的数组   数组temp是归并排序中的额外空间
int ans;
long long T;
int n,m;
long long sq(long long 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++)
        t[i] = w[i];    // 将 w 数组的 [l, r) 区间复制到 t 的 [l, r) 区间中
    sort(t+mid,t+r);     // 将 t 的 [mid, r) 排序
    int i=l,j=mid,index=0;  // 双指针进行区间合并
    while(i!=mid&&j!=r){           // 当 i 不到 mid 且 j 不到 r 时,执行循环
        if(t[i]<=t[j]) temp[index++] = t[i++];
        else temp[index++] = t[j++];
    }
    while(i!=mid) temp[index++] = t[i++];
    while(j!=r) temp[index++]= t[j++];
    long long sum = 0;
    for(int i=0;i<m&&i<index;i++,index--){
        sum+=sq(temp[index-1]-temp[i]);     // 计算校验值
    }
    return sum<=T;       // 返回当前区间 [l, r) 是否合法
}
int main(){
    int k;
    cin>>k;
    while(k--){
        cin>>n>>m>>T;
        for(int i=0;i<n;i++){
            cin>>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] = temp[i-start];  // 复制的时候注意下标变换,tmp 是从 0 开始存的,t 是从 start 开始存的
                    }
                }
                else len>>=1;
            }
            start = end;
            ans++;
        }
        cout<<ans<<endl;
    }
    return 0;
}

ST算法:
在RMQ问题(区间最值问题)中,著名的ST算法就是倍增的产物。给定一个长度为N的数列A,ST算法能在O(NlogN)时间的预处理后,以O(1)的时间复杂度在线回答“数列A中下标在l~r之间的数的最大值是多少”这样的区间最值问题。
设F[i,j]表示数列A中下标在子区间[i,i+(2^j)-1]里的数的最大值,也就是从i开始的2^j个数的最在值。递推边界显然是F[i,0] = A[i],即数列A在子区间[i,i]里的最在值。
在递推时,我们把子区间的长度成倍增长,有公式F[i,j] = max{ F[i,j-1] ,F[i+2^(j-1), j-1]},即长度为2^j的子区间的最大值是左右两半长度为2^(j-1)的子区间的最大值的中较大的一个。

void ST_prework(){
	for(int i=1;i<=n;i++) f[i][0] = a[i];
	int t = log(n)/log(2)+1;
	for(int j=1;j<t;j++)
		for(int i=1;i<=n-(1<<j)+1;i++)
			f[i][j] = max(f[i][j-1],f[i+(1<<(j-1))][j-1]);
}

当询问任意区间[l,r]的最值时,我们先计算出一个k,满足2^k < r-l+1 <= 2^(k+1),也就是使2的k次幂小于区间长度的前提下最大的k。那么“从l开始的2^k个数”和“以r结尾的2^k个数”这两段一定覆盖了整个区间[l,r],这两段的最大值分别是F[l,k]和F[r-2^k+1,k],二者中较大的那个就是整个区间[l,r]的最值。因为求的是最大值,所以这两段只要覆盖区间[l,r]即可,即使有重叠也没有关系

int ST_query(int l,int r){
	int k = log(r-l+1)/log(2);
	return max(f[l][k],f[r-(1<<k)+1][k]);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这段代码是关于数字时钟的程序,使用了51单片机。 1. `#include<reg51.h>`:包含了51单片机的头文件。 2. `unsigned char hour = 0; unsigned char min = 0; unsigned char sec = 0;`:定义了时、分、秒三个变量,用于记录当前时间。 3. `unsigned char table[10]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};`:定义了一个表,用于将数字转换为数码管上显示的对应数字。 4. `unsigned int num=0;`:定义了一个计数器,用于计时一秒。 5. `unsigned dat;`:定义了一个变量,用于存储串口通信中收到的数据。 6. `void delay(unsigned int t);`:声明了一个延时函数。 7. `void main(void)`:主函数开始。 8. `TMOD=0x21;`:设置定时器模式,使用定时器0为模式1,使用定时器1为模式2。 9. `SCON=0x50;`:设置串口工作模式。 10. `PCON=0x00;`:关闭波特率倍增功能。 11. `TH0=0xFC; TL0=0x18;`:设置定时器0的初值,用于实现1秒的定时。 12. `TH1=0xf4; TL1=0xf4;`:设置定时器1的初值,用于串口通信时的波特率控制。 13. `TR1=1; EA=1; ES=1; PS=1; ET0=1; TR0=1;`:开启定时器1、总中断、串口中断、定时器0中断、定时器0。 14. `while(1)`:进入无限循环。 15. `P2=table[sec%10]&0x7f; //秒的个位 P1=0xFE; delay(200); P1=0xFF;`:将秒的个位数转换为数码管上的数字,并输出到P2口上,同时将P1口的第0位置低,使对应数码管亮起来,延时200毫秒后将P1口的第0位置高,消除闪烁。 16. `P2=table[sec/10]; //秒的十位 P1=0xFD; delay(200); P1=0xFF;`:将秒的十位数转换为数码管上的数字,并输出到P2口上,同时将P1口的第1位置低,使对应数码管亮起来,延时200毫秒后将P1口的第1位置高,消除闪烁。 17. `P2=table[min%10]; //分的个位 P1=0xFB; delay(200); P1=0xFF;`:将分的个位数转换为数码管上的数字,并输出到P2口上,同时将P1口的第2位置低,使对应数码管亮起来,延时200毫秒后将P1口的第2位置高,消除闪烁。 18. `P2=table[min/10]; //分的十位 P1=0xF7; delay(200); P1=0xFF;`:将分的十位数转换为数码管上的数字,并输出到P2口上,同时将P1口的第3位置低,使对应数码管亮起来,延时200毫秒后将P1口的第3位置高,消除闪烁。 19. `P2=table[hour%10]; //时的个位 P1=0xEF; delay(200); P1=0xFF;`:将时的个位数转换为数码管上的数字,并输出到P2口上,同时将P1口的第4位置低,使对应数码管亮起来,延时200毫秒后将P1口的第4位置高,消除闪烁。 20. `P2=table[hour/10]; //时的十位 P1=0xDF; delay(200); P1=0xFF;`:将时的十位数转换为数码管上的数字,并输出到P2口上,同时将P1口的第5位置低,使对应数码管亮起来,延时200毫秒后将P1口的第5位置高,消除闪烁。 21. `void delay(unsigned int t) //延时函数 { unsigned int i; for(i=0;i<t;i++); }`:延时函数,用于实现闪烁效果。 22. `void Time() interrupt 1`:定时器0中断处理函数,用于实现1秒的计时和更新时间。 23. `TH0=0xFC; TL0=0x18;`:设置定时器0的初值,用于实现1秒的定时。 24. `num++;`:计数器加1。 25. `if(num==1000)`:计数器达到1000时,说明已经计时1秒。 26. `sec++;`:秒数加1。 27. `if(sec>=60)`:如果秒数超过60,说明已经过了1分钟。 28. `sec=0; min++;`:将秒数清零,并将分钟数加1。 29. `if(min>=60)`:如果分钟数超过60,说明已经过了1小时。 30. `min=0; hour++;`:将分钟数清零,并将小时数加1。 31. `if(hour>=24)`:如果小时数超过24,说明已经过了1天。 32. `hour=0;`:将小时数清零。 33. `void show(void) interrupt 4`:串口中断处理函数,用于接收上位机发送的指令并执行相应操作。 34. `if(RI==1)`:如果收到了数据。 35. `dat=SBUF; RI=0;`:将收到的数据存储在变量dat中,并将接收标志RI清零。 36. `switch(dat)`:根据收到的数据进行相应的操作。 37. `case 1: TR0=!TR0; break;`:收到数字1,将定时器0的运行状态取反,实现暂停和继续计时的功能。 38. `case 2: hour++;if(hour==24) hour=0;break;`:收到数字2,将小时数加1,如果超过24,则将小时数重置为0。 39. `case 3: min++;if(min==60) min=0;break;`:收到数字3,将分钟数加1,如果超过60,则将分钟数重置为0。 40. `case 4: sec++;if(sec==60) sec=0;break;`:收到数字4,将秒数加1,如果超过60,则将秒数重置为0。 41. `case 5: hour=0;min=0;sec=0;P2=table[0];P1=0xc0;TR0=!TR0;break;`:收到数字5,将时间重置为0,并将数码管显示为0,实现清零功能。 42. `default: break;`:如果收到其他数字,则不进行任何操作。 这就是这段代码的逐行解释。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值