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
思路:实际上本题我们如果关注,那么对于每一个,只需要找到一个,使得最大,也就是找一个符合要求的最大。注意本题可以省掉Arr数组,但是会遇到坑,因为本题说每个分数的绝对值不超过150000,也就是可能为负。另外,我们也可以关注,那么只需要在右边找一个尽可能小的即可。
#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的整数序列,定义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. 本题如果直接枚举每个串是否选择(选与不选有两种),则最大的枚举状态数为,这显然是无法承受的,但是我们可以学习上一题的思路,劈一半的思想,将集合划分为两个部分,于是最大枚举状态数为。
3. 对于出现偶数次,我们可以将每个串看作是一个二进制数,字母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,而为了得到最小长度,我们应该尽量减小这个区间。
证明:现在我们有一个区间[,]满足区间和不小于S,如果说有一个更小的区间[,]比前一个区间更小,且满足,并且由于数组中只有正整数,那么必定会有,如若不然,区间长度必定会大于前者,同时区间和也会扩大。
采用尺取法解决本题,每次尝试让待定区间右边界扩张,知道区间和满足>=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;
}