常见的算法优化策略(本文的题目推荐在Virtual Judge上提交)

1.挖掘数据特性解决问题

Age Sort   UVA - 11462 

思路:其实本题要排序的数据量很大,但是数据规模很小,因此用桶排序(计数排序)即可。

#include<bits/stdc++.h>
using namespace std;

int Count[110];

int main(){
	int n,i,num;
	bool first;
	while( scanf("%d",&n) && n ){
		first = false;
		for(i = 1; i <= n; ++ i){
			scanf("%d",&num);
			++ Count[num];
		}
		for(i =  1; i <= 100; ++ i){
			while( Count[i] ){
				if( first == false ){
					printf("%d",i);
					first = true;
				}else{
					printf(" %d",i);
				}
				--Count[i];
			}
		}
		printf("\n");
	} 
	return 0;
}

2.根据题目要求选择维护什么来简化问题

Open Credit System  UVA - 11078 

思路:实际上本题我们如果关注A_j,那么对于每一个A_j,只需要找到一个A_i( 1<= i <j),使得A_i-A_j最大,也就是找一个符合要求的最大A_i。注意本题可以省掉Arr数组,但是会遇到坑,因为本题说每个分数的绝对值不超过150000,也就是可能为负。另外,我们也可以关注A_i,那么只需要在右边找一个尽可能小的A_j即可。

#include<bits/stdc++.h>
using namespace std;
const int MaxN = 1e5 + 7;

int Max[MaxN],Arr[MaxN];

int main(){
	int n,i,ans,num,T;
	cin >> T;
	while( T -- ){
		cin >> n;
		for(i = 1; i <= n; ++ i){
			cin >> Arr[i];
			if( i == 1 )Max[1] = Arr[1];
			else Max[i] = max(Max[i - 1],Arr[i]);
		}
		ans = Max[1] - Arr[2];
		for( i = 3; i <= n; ++ i){
			ans = max(ans, Max[i - 1] - Arr[i]);
		}
		cout << ans << endl;
	}
	return 0;
}

3.滑动窗口系列问题

1.无冲突问题

Unique Snowflakes  UVA - 11572 

思路:要求找到一段尽可能长的区间,使得区间内的元素不存在相同。

1.区间的扩大缩小显然可以用滑动窗口来实现,于是我们的任务变为判断元素的重复。

2.我们可以用将set引入,用set来做判重,复杂度为O(nlogn)。

3.我们可以记录任意一个元素p的上一次出现位置last[p],当我们要加入的元素为p时,如果last[p]处于当前窗口包含的区间段内,那么为了加上新p,就要一直删到这个旧p位置。于是复杂度降低为O(n)。由于本题p的值跨度很大,因此可以采用哈希表而不用开如此大值域的数组;或者也可以选择将数据排序离散化后,再用数组存储(离散化的优点是不存在哈希表的冲突问题,完全是一一映射;缺点是排序加离散化需要花费一定的时间)。

#include<bits/stdc++.h>
using namespace std;

unordered_map<int,int> Last; 
const int MaxN = 1000000+5;

int Arr[MaxN];

int main(){
	int T,n,i,R;
	long long ans;
	ios::sync_with_stdio(false);
	cin >> T;
	while( T -- ){
		cin >> n;
		ans = 0;
		for(i = 1; i <= n; ++ i){
			cin >> Arr[i];
		}
		Last.clear();
		queue<int> que;
		for(i = 1; i <= n; ++ i){
			while( !que.empty() && Last.find(Arr[i]) != Last.end() && que.front() <= Last[Arr[i]] ){
				que.pop();
			}
	    	// 还没出现过 或者队列为空 或者不在队列里 加入队列 更新最近出现值 
			Last[Arr[i]] = i;
			que.push(i);
			ans = max(ans, (long long)que.size());
		}
		cout << ans << endl;
	}
	return 0;
}

4.实际上本题虽然是一个滑动窗口模型,但不一定非要模拟窗口的行为,可以类似与KMP算法的思想,维护两个区间指针L、R,直接根据新元素进入窗口是否会引起冲突决定L是否要移动并且移动到哪里。由于可以存下Last[p],因此我们可以直接通过双指针来定区间,每次区间右边界扩张时,根据新元素p的Last[p]来确定L是不动还是移动到Last[p]+1。是的你没想错,就是一种加速了的尺取法。

#include<bits/stdc++.h>
using namespace std;

unordered_map<int,int> Last; 
const int MaxN = 1000000+5;

int Arr[MaxN];

int main(){
	int T,n,i,L,R,ans;
	ios::sync_with_stdio(false);
	cin >> T;
	while( T -- ){
		cin >> n;
		ans = 0;
		for(i = 1; i <= n; ++ i){
			cin >> Arr[i];
		}
		Last.clear();
		L = 1;
		for(R = 1; R <= n; ++ R){
			if( Last.find(Arr[R]) != Last.end() && L <= Last[Arr[R]] ){
				L = Last[Arr[R]] + 1;
			}
			Last[Arr[R]] = R;
			ans = max(ans, R - L + 1);
		}
		cout << ans << endl;
	}
	return 0;
}

2.最小值问题

输入正整数k和一个长度为n的整数序列A_1,A_2,...,A_n,定义f(i)表示从第i个元素开始的连续k个元素中的最小值,要求输出f(1),f(2),...,f(n-k+1)。例如,对于序列5 2 6 8 10 7 4,则f(1)=2,f(2)=2,f(3)=6,f(4)=4。

思路:其实这题连续k个元素显然是采用滑动窗口解决问题,因此我们的精力就放在了得到窗口内最小值的问题上。

1. 拿到最值很容易想到优先级队列,但是由于优先级队列不能支持随机查找,因此每一个窗口都需要重建一次

2. 我们可以使用set保存窗口内的值,第一个元素出窗口即删除的时候直接二分查到到该元素进行删除

3. 使用单调队列解决问题。我们看f(3)对应的窗口为6 8 10 7,此时由于6和7都比8 10 小,因此他们两个不可能成为最小值,所以我们只需要保存6和7,因为7后面的情况并不知道。极端一点,我们来看f(4)所对应的部分为8 10 7 4,此时4处在滑动窗口内,我们可知只要4在窗口内,前面的8 10 7就不可能成为窗口内最小值,因此窗口内一定存在比他们小的值4,但是4后面(如果有)的情况我们并不确定。于是我们可以发现在任意时刻,队列都是单调递增的。我们只需要在队列中保存元素值、对应索引,保持队列的单调性,根据索引确定是否删除队头元素即可。

#include<bits/stdc++.h>
using namespace std;
const int MaxN = 1e5 + 7;
struct Node{
	int index,value;
}; 
int Arr[MaxN];
queue<Node> que;

int main(){
	int n,i,k;
	cin >> n >> k;
	for(i = 1; i <= n; ++ i){
		cin >> Arr[i];
	}
	for(i = 1; i < k; ++ i){
		while( !que.empty() && que.front().value >= Arr[i] ){
			que.pop();
		}
		que.push(Node{i,Arr[i]});
	}
	for(i = k; i <= n; ++ i){
		while( !que.empty() && que.front().index <= i - k ){ // 删除不在窗口内的元素 
			que.pop();
		}
		while( !que.empty() && que.front().value >= Arr[i] ){ // 删除不单调的元素 
			que.pop();
		}
		que.push(Node{i,Arr[i]});
		cout << que.front().value << endl;
	}
	return 0;
}

 

4.暴力枚举的优化

1.降低N的幂次     4 Values whose Sum is 0  UVA - 1152 

思路:本题就是让你在四个数组中分别找一个数,使得他们的和为0。

1. 直接四重循环枚举,复杂度O(n^4)。

2. 我们知道,当我们确定了A、B、C之后,要想使得和为0成立,那么一定会有D=-A-B-C,所以我们可以枚举A、B、C并将D数组排序后直接二分查找(当然也可以哈希表),复杂度O(n^3logn)。

3. 实际上我们可以枚举A+B和的n^2种情况(当然可能有和相同的情况,这是个坑,注意),此时由于和为0,一定会有-C-D=A+B,然后判断重复,可以用哈希表,这叫中途相遇法,各自枚举一半,可以将O(n^3logn)降低为O(n^2)。

4. 当增加到5个数组的时候,我们枚举2组,每组2两个,剩下一个用于辅助查询。不去枚举3+2的原因是,我们要让n的幂次尽可能的小,而常数变大是可以接受的(因为很多时候,n的规模非常大,常数的影响很小,因此n的阶数我们需要压住)。

#include<bits/stdc++.h>
using namespace std;
const int MaxN = 4010;
unordered_map<long long,long long> Hash; 

long long A[MaxN], B[MaxN], C[MaxN], D[MaxN];

int main(){
	long long T, n, i, j, ans;
	cin >> T; 
	while( T -- ){
		scanf("%lld", &n);
		ans = 0;
		for(i = 1; i <= n; ++ i){
			cin >> A[i] >> B[i] >> C[i] >> D[i];
		}
		for(i = 1; i <= n; ++ i){
			for(j = 1; j <= n; ++ j){
				if( Hash.find(A[i] + B[j]) == Hash.end() ) Hash[A[i] + B[j]] = 1;
				else Hash[A[i] + B[j]] ++;
			}
		}
		for(i = 1; i <= n; ++ i){
			for(j = 1; j <= n; ++ j){
				if( Hash.find(A[i] + B[j]) != Hash.end() ) ans += Hash[-C[i] - D[j]];
			}
		}
		printf("%lld\n", ans);
		if( T ) printf("\n");
		Hash.clear();
	}
	return 0;
}

2.降低指数  Jurassic Remains  UVALive - 2965 

思路:本题的题意是说给定N个字符串,要求选出最多的字符串,使得每一个字母出现的数量为偶数次。

1. 题目特意强调了偶数次,意在指出本题关注点并不是出现了多少次,而是次数的奇偶性。

2. 本题如果直接枚举每个串是否选择(选与不选有两种),则最大的枚举状态数为2^{24},这显然是无法承受的,但是我们可以学习上一题的思路,劈一半的思想,将集合划分为两个部分,于是最大枚举状态数为2*2^{12}

3. 对于出现偶数次,我们可以将每个串看作是一个二进制数,字母A对应了2^{'A'-'A'}这个权值,依此类推,因此可以用一个26位二进制数表示。选择k个串相当于将他们对应的二进制数抑或起来。那么每个字母出现偶数次即为两个集合生成的二进制数的抑或值为0,也就是两个值相等的情况,于是转换为查询问题。

#include<bits/stdc++.h>
using namespace std;
struct Node{
	int choice, sum; // 选择对应的二进制  选择的串个数 
};

unordered_map<int,Node> Hash; // xor值 对应的字符串数量 

int Num[30];
string strs[30];

int ToNum(string str){
	int len = str.length(), num = 0;
	for(int i = 0; i < len; ++ i){
		num ^= 1 << (str[i] - 'A'); 
	}
	return num;
}

int main(){
	int n, i, k, mid1, mid2, ans, sum, Xor, choice1, choice2; 
	while( cin >> n ){
		Hash.clear();
		ans = 0; // 最小情况就是一个都选不了 
		for(i = 1; i <= n; ++ i){
			cin >> strs[i];
			Num[i] = ToNum( strs[i] );
		}
		// 生成前一个集合对应的Hash全集 
		mid1 = n >> 1;
		for(i = 1; i < 1 << (mid1); ++ i){
			sum = 0; // 选择的串个数 
			Xor = 0;
			for(k = 1; k <= mid1; ++ k){
				if( (1 << ( k - 1) ) & i ){ //选择第k个数 
					++ sum;
					Xor ^= Num[k];
				}
			}
			if( Hash.find(Xor) != Hash.end() && Hash[Xor].sum > sum ){ //如果已经存入的相同Xor值的串更多 就不覆盖 
				continue;
			} 
			Hash[Xor] = Node{i, sum};
		} 
		// 生成后一个集合对应的Hash全集 
		mid2 = n - mid1;
		for(i = 1; i < 1 << (mid2); ++ i){
			sum = 0; // 选择的串个数 
			Xor = 0;
			for(k = 1; k <= mid2; ++ k){
				if( (1 << ( k - 1) ) & i ){ //选择第mid + k个数 
					++ sum;
					Xor ^= Num[mid2 + k];
				}
			}
			if( Hash.find(Xor) != Hash.end() && Hash[Xor].sum + sum > ans ){
				ans = Hash[Xor].sum + sum;
				choice1 = Hash[Xor].choice;
				choice2 = i; 
			}
		} 
		cout << ans << endl;
		for(k = 1; k <= mid1; ++ k){
			if( (1 << ( k - 1) ) & choice1 ){
				cout << k << " ";
			}
		}
		for(k = 1; k <= mid2; ++ k){
			if( (1 << ( k - 1) ) & choice2 ){ //选择第mid + k个数 
				cout << mid2 + k << " ";
			}
		}
		cout << endl;
	}
	return 0;
}

5.子序列性质问题-尺取法

思路:题意是给你n个正整数,要求你求出满足区间和不小于S的最小长度的区间长度。首先,这是一个正整数序列 ,因此我们知道,对于一个区间[L,R]来说,如果他本身已经满足不小于S了,那么加上任何元素都一定满足不小于S,而为了得到最小长度,我们应该尽量减小这个区间。

证明:现在我们有一个区间[L_1,R_1]满足区间和不小于S,如果说有一个更小的区间[L_2,R_2]比前一个区间更小,且满足R_2>=R_1,并且由于数组中只有正整数,那么必定会有L2>=L1,如若不然,区间长度必定会大于前者,同时区间和也会扩大。

采用尺取法解决本题,每次尝试让待定区间右边界扩张,知道区间和满足>=S,此时缩小区间,直到区间和<S,此时R-L+2就是对答案的贡献,更新答案即可。

#include<bits/stdc++.h>
using namespace std;
const int MaxN = 1e5 + 7; 

int Arr[MaxN];

int main(){
	int n, s, ans, L, R, sum, i;
	while( cin >> n >> s ){
		memset(Arr,0,sizeof(Arr));
		L = 1;
		ans = n;
		sum = 0;
		for(i = 1; i <= n; ++ i){
			cin >> Arr[i];
		}
		for(R = 1; R <= n; ++ R){
			if( ( sum += Arr[R] ) < s ){
				continue;
			}
			while( sum >= s ){
				sum -= Arr[L];
				++ L;
			}
			ans = min(ans, R - L + 2);
		}
		cout << ans << endl; 
	}
	return 0;
}

6.思维拓展

思路:实际上算法优化的本质就是利用一些已经做过处理的数据,对待处理数据的处理过程进行加速。

本题题意是在一个矩形中找到存在的最大矩形的面积(别忘了题目要求*3再输出)。

1. 枚举:我们需要枚举左上角点与对应矩形的高和宽,还需要判断是否合法,于是复杂度为O(n^3*m^3)。

2. 悬线法:我们要计算一个矩形的面积,其实就是要知道它的高和宽。我们将待求矩形看作是由一条悬线移动形成的,首先,对于方块(i,j),我们将他向上可以延伸的高度为一条悬线,此时我们如果再知道这条悬线可以向左向右移动的距离,就可以得到这条悬线对应的最大矩形的面积。

然后我们来看看为什么这个算法可以不重复不遗漏地计算最大值。首先,我们要计算的是最大矩形的面积,因此,固定高度后尽可能向左向右延伸肯定是该悬线对应的最大矩形面积。然后我们来看是否遗漏的问题。很显然,悬线的高度越大,他就越容易和撞到左右两边的障碍物,如果减小悬线高度,那么宽度就很有可能增加,所以怎么说明他们是不遗漏的呢?我们来看下图。

在该图中,对于(i,j)格,它对应的悬线高度为2,但是以2为悬线高度左右扩展,只能得到宽度为1的矩形,但显然此时如果让悬线高度为1,那么可以得到面积值为3的矩形。但是呢,实际上这个面积值为3的矩形是可以被(i,j-1)和(i,j+1)探测到的。其他情况也如此,由于高度固定,我们知道,当两条悬线相邻时,其中更长的那一条不会影响短悬线的左右扩展,相反,短悬线会影响长悬线的扩展,因此长悬线没有考虑到的都在短悬线考虑的情况里。于是我们一行一行扫描统计维护即可。我们只需要维护up(i,j)表示以(i,j)为底的悬线最大高度,left(i,j)表示该悬线左边不能扩展到的最左位置(墙壁),right(i,j)表示悬线不能向右扩展到的最大位置。

我们基以下逻辑完成数据维护:

      如果当前格子是R(也就是墙壁),那么它对应的up、left均为0,right为m+1。

      如果当前格子是F(空地),显然它的up取决于它上面那个元素的up值,即up(i,j)=up(i-1,j)。left与前面行和当前行均有关,但是(1~i-1)行的考虑已经包含在left(i-1,j)中了,因此还需要知道(i,j)左边最近墙壁的位置locL,left(i,j)=max(left(i-1,j),locL)。

      那么同理我们可以知道右边的更新方式为right(i,j)=min(right(i-1,j),locR)。

      那么(i,j)这个悬线对应的矩形的面积为    ( right[j] - left[j] - 1 )  * up[j]

注意:也许会有人觉得只计算up和left就可以了,但是是不行的,来看以下一个例子:

如果我们只计算left和up,只能够得到答案2(题目输出还要乘3),因为真正的答案是(i,j-1)各左右扩展1个位置,但是只计算左边就会少掉右边的扩展!!!需注意!!!因此本人采用了两次扫描的办法,第一遍生成up与left,第二遍生成right同时计算面积,虽然你可以把它们写成双向扫描,但是分开写代码更加清晰。(尤其是一边扫描,right的生成与前两者并不同步,还是需要一个循环计算面积)。

另外请注意:right数组由于要取小,因此初始化时要足够大,同理up与left取大,初始化时默认取0即可。

#include<bits/stdc++.h>
using namespace std;

const int MaxN = 1010;
int Up[MaxN], Left[MaxN], Right[MaxN];
// up:悬线高度  left:左边最近障碍物坐标 right:右边最近障碍物坐标 
/*
1
3 5
R F R F F
F R F R F
F F F R R 
*/
char Map[MaxN][MaxN];

int main(){
	int T, n, m, i, j, area, L, R, h, LocL, LocR;
	cin >> T;
	while( T -- ){
		cin >> n >> m;
		area = 0;
		memset(Up, 0, sizeof(Up));
		memset(Left, 0, sizeof(Left));
		memset(Right,0x3f,sizeof(Right));
		for(i = 1; i <= n; ++ i){
			for(j = 1; j <= m; ++ j){
				cin >> Map[i][j];
			}
		}
		for(i = 1; i <= n; ++ i){
			LocL = 0;
			for(j = 1; j <= m; ++ j){
				if( Map[i][j] == 'R' ){
					Up[j] = Left[j] = 0;
					LocL = j;
				}else{
					Up[j] = Up[j] + 1;
					Left[j] = max(Left[j], LocL );
				}
			}
			LocR = m + 1;
			for(j = m; j >= 1; -- j){
				if( Map[i][j] == 'R' ){
					Right[j] = LocR;
					LocR = j;
				}else{
					Right[j] = min(LocR, Right[j]);
				}
				area = max( area, ( Right[j] - Left[j] - 1 ) * Up[j] );
			}
		}
		cout << area * 3 << endl;
	}
	return 0;
}

2. 单调栈:实际上,单调栈不是采用递推方式维护信息,而是使用单调栈的方式得到左右最近的比当前值小的元素,总体思想与上述方法差不多,其实对于一条悬线,左右能扩展的最大位置取决于左右最近的比它小的那个悬线的位置。

我们维护一个单调递增的栈:相同的元素可以做忽略处理,因为如果两块相邻元素拥有相同高度的话,那么他们左右扩展后形成的最大矩阵是同一个,但是在代码实现上,我们需要保留最新的那个,下面会解释:

A. 从左到右扫描高度

B. 当栈不为空且栈顶元素的高度大于等于当前要进入栈的元素时,需要将栈顶元素出栈,同时栈顶元素的下一个元素即暗示了栈顶元素的左边界(如果出栈后栈空则可以该元素可以向左扩展到最左边,右边界就是当前要进栈元素的索引),计算面积更新答案。

C. 将新元素入栈,重复A-C直到扫描结束

D. 扫描结束后可知,栈内所有元素的右边界为最右边,左边界为他们在栈中的下一个元素(栈底元素为最左边界)。

可以知道,如果对于相同高度h,我们保留先进栈的那个,那么对于右边高度比h大的矩阵,他们的左边界计算会出现问题,实际上他们应该碰触到后进来的那个高度为h的元素。另外,我们可以把非方块部分当作高度为0,这样即使它参与计算,得出的面积值也是0,不会产生实质上的影响。

#include<bits/stdc++.h>
using namespace std;
const int MaxN = 1010;

struct Node{
	int height,index;
};

char Map[MaxN][MaxN];
int height[MaxN];

int main(){
	int T, n, m, i, j, area, L, R, h;
	cin >> T;
	while( T -- ){
		cin >> n >> m;
		area = 0;
		memset(height, 0, sizeof(height));
		for(i = 1; i <= n; ++ i){
			for(j = 1; j <= m; ++ j){
				cin >> Map[i][j];
			}
		}
		
		stack<Node> ST;
		for(i = 1; i <= n; ++ i){
			for(j = 1; j <= m; ++ j){
				height[j] = Map[i][j] == 'R' ? 0 : height[j] + 1;
				while( !ST.empty() && ST.top().height >= height[j] ){
					R = j;
					h = ST.top().height;
					ST.pop();
					L = ST.empty() ? 0 : ST.top().index; 
					area = max(area, (R - L - 1) * h);
				} 
				ST.push(Node{height[j], j});
			}
			R = n + 1;
			while( !ST.empty() ){
				h = ST.top().height;
				ST.pop();
				L = ST.empty() ? 1 : ST.top().index; 
				area = max(area, (R - L) * h);
			}
		}
		cout << area * 3 << endl;
	}
	return 0;
}

7.从最大问题出发得到优化方法

思路:题意是,对于一个序列,如果它的任意子序列都有一个元素只出现一次,那么他是non-boring的,否则就是boring的。

1. 很显然,如果我们要枚举所有子串再判断的话,枚举需要O(n^2),判断需要O(n),也可以用前缀和优化判断。

2. 我们先看整体的串,为了使得这个最大的串non-boring,我们可以知道,这个串中一定存在一个元素只出现了一次。那么根据这一条件,假设这个元素出现在A[i],那么可以知道,所有包含A[i]的串必定是non-boring的,因此我们只需要考虑A[1]~A[i-1]与A[i+1]~A[n],而对于这两个串,如果他们是无聊的,那么必定满足A[1]~A[i-1]和A[i+1]~A[n]中只有一个元素出现了一次,不断递归下去。

3. 显然如果上述算法的时间开销都花在找这个出现一次的元素上了,因此我们只需要优化这个时间开销即可。事实上,我们可以记录下每个元素最左和最右的与他相同值的元素的位置,如果最近的元素,有存在当前区间里的,那么他就不是这个唯一元素。

4. 但是现在我们会遇到新的问题,如果我们从左边开始搜索,可能唯一的元素存在区间右边界上,此时搜素需要O(n),算法会退化成O(n^2)。那我们如果从右边开始搜索,也会遇到唯一元素在左边界附近的情况,那么我们可以双向搜索,都往中间走,谁先碰到唯一元素谁就负责划分区间,于是最坏情况是元素处在中间时,此时正好将区间分为一半,复杂度为O(nlogn)。即最好复杂度为都在边界上,为O(n),最差则在中间,为O(nlogn)。

#include<bits/stdc++.h>
using namespace std;
const int MaxN = 200005;

int Left[MaxN], Right[MaxN], Arr[MaxN];
unordered_map<int,int> LocLeft,LocRight;

bool Solve(int L,int R){
	if( L >= R )return true;
	int p = L, q = R;
	while( p <= q ){
		if(Left[p] < L && Right[p] > R){
			return  Solve(L,p - 1) && Solve(p + 1,R);
		}
		++ p;
		if(Left[q] < L && Right[q] > R){
			return  Solve(L,q-1) && Solve(q+1,R);
		}
		-- q;
	}
	return false; 
}

int main(){
	int T, n, i;
	cin >> T;
	while( T -- ){
		cin >> n;
		LocLeft.clear();
		LocRight.clear();
		for(i = 1; i <= n; ++ i){
			cin >> Arr[i];
			Left[i] = LocLeft[Arr[i]];
			LocLeft[Arr[i]] = i;
		}
		for(i = n; i >= 1; -- i){
			Right[i] = LocRight[Arr[i]] == 0 ? n + 1 : LocRight[Arr[i]];
			LocRight[Arr[i]] = i;
		}
		if( Solve(1, n) ) cout << "non-boring" << endl;
		else cout << "boring" << endl;
	}
	return 0;
}

8.快慢指针法

Calculator Conundrum  UVA - 11549

思路:本题题意是告诉我们在题意所示的是运算规则下,得到的数会产生重复,要求我们求出这个过程中能得到的最大值。

1. 其实很显然,我们可以用哈希表存储已经访问过的元素,当我们第二次访问到已有的元素时,即表明我们已经访问完了这个循环圈。

2. 但是我们没办法知道这个圈可能会有多大,有可能会被空间限制卡掉。因此介绍一种快慢指针法,我们可以想象在一个操场上,有着一快一慢两个小朋友在跑步,我们可以让他们跑任意圈,那么跑得快的人终将会追上跑得慢的人(相遇时快的人比慢的人多跑一圈)。于是我们可以设计快慢指针,每次让快指针向前走两步,慢指针向前走一步,于是当他们相遇时,一定遍历完了所有的点。

我们来看一个简单的例子:

如果循环圈长度为奇数 0~6

快指针: 0-2-4-6-1-3-5-1

慢指针: 0-1-2-3-4-5-6-1

如果循环圈长度为偶数 0~7

快指针: 0-2-4-6-1-3-5-7

慢指针: 0-1-2-3-4-5-6-7

当然,更为保险的做法是,对于快指针的一大步中的两小步,都让他更新答案,因为当二者相遇时,快指针一定走遍了整个循环圈。

#include<bits/stdc++.h>
using namespace std;
int buf[110];

long long Next(long long k,int n){
	long long temp,res;
	temp = res = k * k;
	int cnt = 0, i;
	while( res ){
		buf[cnt ++] = res % 10;
		res /= 10;
	}
	if( cnt < n ) return temp; // 长度不够取模
	for( i = 1; i <= n; ++ i){
		res = res * 10 + buf[-- cnt];
	}
	return res;
}

int main(){
	int T, n;
	cin >> T;
	long long k, lower, faster, ans;
	while( T -- ){
		cin >> n >> k;
		lower = faster = ans = k;
		do{
			lower = Next(lower,n);
			faster = Next(faster,n); ans = max(faster,ans);
			faster = Next(faster,n); ans = max(faster,ans);
		}while( lower != faster );
		cout << ans << endl;
	}
	return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值