一、九宫幻方(DFS,2017)
题目描述
小明最近在教邻居家的小朋友小学奥数,而最近正好讲述到了三阶幻方这个部分,三阶幻方指的是将 1~9 不重复的填入一个 3*3 的矩阵当中,使得每一行、每一列和每一条对角线的和都是相同的。
三阶幻方又被称作九宫格,在小学奥数里有一句非常有名的口诀:"二四为肩,六八为足,左三右七,戴九履一,五居其中",通过这样的一句口诀就能够非常完美的构造出一个九宫格来。
4 9 2 3 5 7 8 1 6
有意思的是,所有的三阶幻方,都可以通过这样一个九宫格进行若干镜像和旋转操作之后得到。现在小明准备将一个三阶幻方(不一定是上图中的那个)中的一些数抹掉,交给邻居家的小朋友来进行还原,并且希望她能够判断出究竟是不是只有一个解。
而你呢,也被小明交付了同样的任务,但是不同的是,你需要写一个程序。
输入描述
输入仅包含单组测试数据。
每组测试数据为一个 3*3 的矩阵,其中为 0 的部分表示被小明抹去的部分。
给出的矩阵至少能还原出一组可行的三阶幻方。
输出描述
如果仅能还原出一组可行的三阶幻方,则将其输出,否则输出"Too Many"(不包含引号)。
思路
题意转换:给定一个3 * 3 的矩阵,矩阵内所有元素的值的范围均为0~9。现要求用1~9中的某一个数替换矩阵中数值为0的元素,替换后的矩阵满足以下条件:
矩阵中每一行、每一列、每一条对角线的和相等
矩阵中所有元素的值的范围均为1~9
矩阵中没有值相同的元素
全排列。如果我们把这个矩阵按行展开为一行元素的话,我们会发现生成这样一个完全由1~9内的数字组成且没有重复数字的过程实际上就是生成一个全排列的过程。只不过在生成全排列的基础之上有一些限制条件(需满足矩阵中的每一行、每一列和每一条对角线的和相等)。
首先,生成出所有的全排列
将全排列转化为全排列矩阵
判断全排列矩阵的非0部分与输入的矩阵是否相同
若相同,则在此基础上判断其每一行、每一列、每一条对角线的和是否相等
#include <iostream>
#include <algorithm>
using namespace std;
int path[10];
int cnt;
int a[3][3], b[3][3], ans[3][3];
int main()
{
for(int i = 0; i < 3; i ++ )
for(int j = 0; j < 3; j ++ )
cin >> a[i][j];
for(int i = 1; i <= 9; i ++ ) path[i] = i;
do
{
b[0][0] = path[1], b[0][1] = path[2], b[0][2] = path[3];
b[1][0] = path[4], b[1][1] = path[5], b[1][2] = path[6];
b[2][0] = path[7], b[2][1] = path[8], b[2][2] = path[9];
bool flag = true;
for(int i = 0; i < 3; i ++ )
for(int j = 0; j < 3; j ++ )
{
if(a[i][j] != 0 && a[i][j] != b[i][j]) flag = false;
}
if(!flag) continue;
bool ok = true;
int sum = b[0][0] + b[1][1] + b[2][2];
if(b[0][2] + b[1][1] + b[2][0] != sum) continue;
for(int i = 0; i < 3; i ++ )
{
int temp1 = 0, temp2 = 0;
for(int j = 0; j < 3; j ++)
{
temp1 += b[i][j], temp2 += b[j][i];
}
if(temp1 != sum || temp2 != sum) ok = false;
}
if(!ok) continue;
cnt ++;
if(cnt >= 2)
{
cout << "Too Many" << endl;
exit(0);
}
for(int i = 0; i < 3; i ++ )
for(int j = 0; j < 3; j ++ )
ans[i][j] = b[i][j];
}while(next_permutation(path + 1, path + 9 + 1));
for(int i = 0; i < 3; i ++ )
{
for(int j = 0; j < 3; j ++ ) cout << ans[i][j] << " ";
puts("");
}
return 0;
}
/*
利用next_permutation生成全排列,共有9!中排列方案
next_permutation函数的功能是将数组中选定范围的数按照字典序进行全排列。
*/
DFS。搜索顺序:依次对所有为0的位置进行修改。
将矩阵中所有元素为0的位置存储下来
将矩阵中已出现过的数打上标记
对于每个被存储下来的位置,依次尝试将其值修改为未被标记过的数(即未使用过的数)
待填充完矩阵中所有为0的位置之后,判断一下该矩阵是否为九宫幻方。若为九宫幻方,将其记录下来并判断九宫幻方的个数是否已经大于1。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef pair<int, int> PII;
int n, cnt;
int a[3][3], ans[3][3], vis[10];
PII s[10];
bool check()
{
int sum = a[0][0] + a[1][1] + a[2][2];
if(a[0][2] + a[1][1] + a[2][0] != sum) return false;
for(int i = 0; i < 3; i ++ )
{
int temp1 = 0, temp2 = 0;
for(int j = 0; j < 3; j ++ )
{
temp1 += a[i][j];
temp2 += a[j][i];
}
if(temp1 != sum || temp2 != sum) return false;
}
return true;
}
void dfs(int u)
{
if(u == n)
{
if(check())
{
cnt ++;
for(int i = 0; i < 3; i ++ )
for(int j = 0; j < 3; j ++ )
ans[i][j] = a[i][j];
}
return;
}
int x = s[u].first, y = s[u].second;
for(int i = 1; i <= 9; i ++ )
{
if(vis[i]) continue;
a[x][y] = i;
vis[i] = 1;
dfs(u + 1);
a[x][y] = 0;
vis[i] = 0;
}
}
int main()
{
for(int i = 0; i < 3; i ++ )
for(int j = 0; j < 3; j ++ )
{
cin >> a[i][j];
if(a[i][j] == 0) s[n ++] = make_pair(i, j);
vis[a[i][j]] = 1;
}
dfs(0);
if(cnt == 1)
{
for(int i = 0; i < 3; i ++ )
{
for(int j = 0; j < 3; j ++ )
cout << ans[i][j] << " ";
puts("");
}
}
else
{
cout << "Too Many" << endl;
}
return 0;
}
二、小朋友崇拜圈(DFS,2018)
题目描述
班里 N个小朋友,每个人都有自己最崇拜的一个小朋友(也可以是自己)。
在一个游戏中,需要小朋友坐一个圈,每个小朋友都有自己最崇拜的小朋友在他的右手边。
求满足条件的圈最大多少人?
小朋友编号为 1,2,3, ......,N。
输入描述
输入第一行,一个整数 N(3 < N < ) 。
接下来一行 N个整数,由空格分开。
输出描述
要求输出一个整数,表示满足条件的最大圈的人数。
输入输出样例
输入
9
3 4 2 5 3 8 4 6 9
输出
4
思路
这是一道经典的DFS找环问题。
对于题目中提到的“小朋友”“圈”,我们分别用“点”“环”来代替它们的含义。
根据“每个小朋友可以可崇拜的对象只有一个(但一个小朋友可能被多个小朋友崇拜),我们可以得到一个信息:每个小朋友最多只可能属于一个环。
我们可以通过反证法来证明:如果一个小朋友属于多个环,那么每个环内必然要有一个他的崇拜对象,即他要有多个崇拜对象。这并不符合题意,故假设不成立。
我们以样例为例,来分析一下当前这个题目:我们可以将小朋友用节点表示,小朋友之间的关系用箭头表示。则小朋友之间的崇拜关系为:1 -> 3,2 -> 4, 3 -> 2, 4 -> 5,5 -> 3,6 -> 8,7 -> 4,8 -> 6,9 -> 9。
不难发现,当红色箭头出现以后,图中出现了环。我们可以称红色箭头出现的边为环边。
环边出现以后,环也会出现,环的大小等于环边两端的节点之间的箭头数。
求出环的大小是解题的最终目的,确定了所有环的大小,就能确定答案。
怎么判断一条边是不是环边?
环边指向的节点一定是已经出现过的节点,我们可以通过为所有出现过的节点打上标记以判断一条边是否是环边。
怎么求环边两端节点之间的箭头数?
可以为每个箭头设置一个编号。对于环内的每一条边,它们的编号一定是连续的。因此,环内的箭头数等于[环边的箭头编号 - 第一条边的箭头编号 + 1]。
找到环以后,是否还需要继续移动?
当然不需要,找到环之后,接着移动就相当于在这个环上又重复走了一遍。
DFS的遍历顺序:从每一个没有被标记过的节点开始遍历,直到所有的节点都被遍历过为止
/*
1.从每一个没有被标记过的节点开始,一直走,直到出现环边,停止。
2.走的同时为节点打上标记,并记录边的编号
3.当走到被其他路线标记过的点后,将走重复的路,没有意义,停止。
4.出现环边后计算环的大小并更新答案
next[x]:表示x崇拜的对象
number[x]:表示x -> number[x]这条边
*/
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
int n, ans;
int ne[N], number[N], vis[N];
void dfs(int x, int cnt, int id) //x表示当前走到的节点,cnt表示当前边的编号,id表示当前路线的编号
{
if(vis[x] && vis[x] != id) return;
if(vis[x]) //在同一条路线上
{
ans = max(ans, (cnt - 1) - number[x] + 1); //cnt - 1是环边的编号,number[x]是重复的小朋友最开始所在的边的编号(不清楚的可以自己手动模拟一遍)
return;
}
//说明既不是环边也没被标记过
vis[x] = id;
number[x] = cnt;
dfs(ne[x], cnt + 1, id);
}
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i ++ ) scanf("%d", &ne[i]);
for(int i = 1; i <= n; i ++ ) dfs(i, 1, i); //边的编号每次都是从1开始的
printf("%d\n", ans);
return 0;
}
三、迷宫与陷阱(BFS,2018)
题目描述
小明在玩一款迷宫游戏,在游戏中他要控制自己的角色离开一间由 N * N个格子组成的 2D 迷宫。
小明的起始位置在左上角,他需要到达右下角的格子才能离开迷宫。
每一步,他可以移动到上下左右相邻的格子中(前提是目标格子可以经过)。
迷宫中有些格子小明可以经过,我们用 '.' 表示。
有些格子是墙壁,小明不能经过,我们用 '#' 表示。
此外,有些格子上有陷阱,我们用 'X' 表示。除非小明处于无敌状态,否则不能经过。
有些格子上有无敌道具,我们用 '%' 表示。
当小明第一次到达该格子时,自动获得无敌状态,无敌状态会持续 K步。
之后如果再次到达该格子不会获得无敌状态了。
处于无敌状态时,可以经过有陷阱的格子,但是不会拆除/毁坏陷阱,即陷阱仍会阻止没有无敌状态的角色经过。
给定迷宫,请你计算小明最少经过几步可以离开迷宫?
输入描述
第一行包含两个整数 N, K(1 <= N <= 1000, 1 <= K <= 10)。
以下 N行包含一个N * N 的矩阵。
矩阵保证左上角和右下角是 '.'。
输出描述
一个整数表示答案。如果小明不能离开迷宫,输出 -1。
思路
这是一道BFS解迷宫的变种题。对于迷宫的坐标,我们习惯用二维坐标来表示:(x, y)表示位于第x行第y列的格子。
分析一下迷宫上格子的类型:
a.“.”:普通的格子,小明可以经过
b.“#”:墙壁,小明不能经过
c.“%”:拥有无敌道具的格子,小明经过该格子之后会进入无敌状态并持续K步,但格子会变为普通的格子
d.“X”:陷阱,如果处在无敌状态则可以经过,否则不可以经过
移动时需要记录的信息
1)记录移动的坐标 2)记录移动的步数 3)移动到下一个位置时的状态
对于这些信息如果从一个位置(x,y)移动到下一个位置,则:
1)坐标会变为(x - 1, y)或(x, y + 1)或(x + 1,y)或(x,y - 1)。
2)移动的步数为加一
3)如果是普通状态,则可能维持普通状态,也可能变为无敌状态;如果是无敌状态,则可能维持无敌状态,也可能变为普通状态。
由此我们可以看出,该题状态的表示有很多种。需要注意的是,当处于无敌状态的小明再次捡到无敌道具时,其无敌状态的剩余步数会刷新为K步。
还有一点需要注意的是:在本道题中格子可以重复走。(因为处于无敌状态后可以通过先前通过不了的路,这使得我们即使往回走了也可能使得当前的路的步数最少)
那对于有些点是否有重复走的必要呢?(最优性剪枝)
判断有没有必要,主要取决于第二次走到该点时的信息是否优于第一次走到该点时的信息,如果优于则有必要,反之则没有必要。
那我们该如何判断移动信息的优劣呢?
显然,移动的步数越少越好,可保持的无敌状态越久越好。
假设上一次走到(x,y)时,有以下两种情况。
a. 移动的步数为cnt1。
b. 接下来的status1步都将保持无敌状态。
此次走到(x,y)时有以下两种情况。
a. 移动的步数为cnt2。
b. 接下来的status2步都将保持无敌状态。
若cnt1 < cnt2且status1 > status2,完全没有必要再走一次;
若cnt1 < cnt2且status1 < status2,我们无法判断哪种情况更优,因此是有必要再走一次的;
若cnt1 > cnt2且status1 > status2,我们无法判断哪种情况更优,因此是有必要再走一次的;
若cnt1 > cnt2且status1 < status2,完全有必要再走一次。
/*
对于没有走过的格子,我们需要遍历一下
对于走过的格子,我们需要判断一下有没有再走的必要
vis[][]数组是标记当前这个点有没有被访问过
s[][]中存储的是在这个点处的可保持无敌状态的最大步数
*/
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1010;
struct node
{
int x, y;
int cnt;
int status;
};
int n, k;
char g[N][N];
int vis[N][N], s[N][N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int bfs()
{
queue<node> q;
q.push({0, 0, 0, 0});
vis[0][0] = 1;
while(!q.empty())
{
auto t = q.front();
q.pop();
if(t.x == n - 1 && t.y == n - 1) return t.cnt;
for(int i = 0; i < 4; i ++ )
{
int a = t.x + dx[i], b = t.y + dy[i];
if(a < 0 || a >= n || b < 0 || b >= n || g[a][b] == '#') continue;//不能越界且遇到墙壁不能通过
if(g[a][b] == 'X' && t.status == 0) continue;//处于普通状态时遇到障碍物
//成功走到下一格,为了少写一步,我们就假设上一步都处于无敌状态吧
int status = max(0, t.status - 1);
if(g[a][b] == '%')//道具格子,如果碰到道具的话,证明这个格子一定是第一次走到
{
vis[a][b] = 1;
g[a][b] = '.';
q.push({a, b, t.cnt + 1, k});
}
else//否则就是普通格子
{
if(!vis[a][b])
{
vis[a][b] = 1;
q.push({a, b, t.cnt + 1, status});
continue;
}
if(status <= s[a][b]) continue;//可保持的无敌状态劣于上一次,没有必要再走一次
s[a][b] = status;
q.push({a, b, t.cnt + 1, max(0, status)});
}
}
}
return -1;
}
int main()
{
cin >> n >> k;
for(int i = 0; i < n; i ++ )
for(int j = 0; j < n; j ++ )
cin >> g[i][j];
cout << bfs() << endl;
return 0;
}
四、扫地机器人(贪心、枚举、二分,2019)
题目描述
小明公司的办公区有一条长长的走廊,由 个方格区域组成,如下图所示。
R | R | R |
走廊内部署了K台扫地机器人,其中第 i台在第 A_i个方格区域中。已知扫地机器人每分钟可以移动到左右相邻的方格中,并将该区域清扫干净。
请你编写一个程序,计算每台机器人的清扫路线,使得
1. 它们最终都返回出发方格, 2. 每个方格区域都至少被清扫一遍, 3. 从机器人开始行动到最后一台机器人归位花费的时间最少。
注意多台机器人可以同时清扫同一方块区域,它们不会互相影响。
输出最少花费的时间。 在上图所示的例子中,最少花费时间是 6。第一台路线:2 - 1 - 2 - 3 - 4 - 3 - 2,清扫了 1、2、3、4 号区域。第二台路线 5 - 6 - 7 - 6 - 5,清扫了 5、6、7。第三台路线10 - 9 - 8 - 9 - 10 ,清扫了 8、9 和 10。
输入描述
第一行包含两个整数 N,K。
接下来 K行,每行一个整数 A_i。 其中,1 <= K < N <= ,1 <= <= N 。
输出描述
输出一个整数表示答案。
思路
本题解题的三个关键信息如下:
a. 清扫的时间
b. 清扫的方法
c. 清扫的区域面积(个数)
其中清扫的最短时间是我们要求的答案,清扫的区域面积是题目已知的,因此我们需要决定的是清扫的方法。显然,我们需要找到一个最优的清扫方法,使得我们在每个区域都至少被清扫一次的情况下清扫的总时间最少。
求最优的清扫方法:
假设目前存在k台机器人,它们从左到右的编号分别为1,2,3,......,k,每台机器人清扫每个区域的时间为t。
按照顺序,我们将从1号机器人左边的区域开始清扫(当然,1号机器人左边的区域可能为空)。我们有k的机器人,该让哪个机器人过来清扫效率会更高呢?显然是1号机器人,因为它离这片区域最近,耗费的时间最少。
当1号机器人清扫完左边的区域回到原位以后,如果还有剩余的时间,我们不能浪费,要让它接着清扫它右边的区域。这样可以减少下一个机器人向左清扫的时间。
每次一个机器人清扫结束之后,我们要将已经清扫过的区域和当前的机器人忽略掉。这样,原来的第二个机器人成为了新的第一个机器人,区域也更新为了新的一片未清扫过的区域。
因此,可根据贪心的思想确定清理方法:
让每台机器人优先清扫其左边还未扫过的格子,然后再往右扫,以保证每次清扫的效率最高。
求最少的清理时间
在开始求最少清理时间之前,我们需要明确两点:
如果所有机器人,在时间t内,都用最优秀的清理方法,却没能清理完所有的区域,那么问题就只能出在“t太小(时间不够)”上。
如果所有机器人,都用最优秀的清理方法,那么给的时间越多,可清理的区域只会多不会少(在某个临界点之后,随着时间的增加,可清理的区域个数将不会变化)
由上,我们可以得出:对于一个时间t,它想要作为答案需满足两个条件:
a. 所有机器人在t分钟内可清理所有区域
b. t是所有满足条件a的时间中,最小的一个
对于条件a,我们可以模拟清扫的方法,从1号区域开始一个个清扫区域。在所有机器人清扫结束后判断是否清扫了所有区域即可。
对于条件b,我们可以从小到大去枚举t,这样第一个满足条件a的t,就是答案。
优化时间复杂度
已知答案含义为:在使用最高效方法的前提下,清扫所有区域所需要花费的最少时间。
而根据前面的分析可得:随着时间增加,可清理的区域只会多不会减。
由此可知:时间是满足单调性的。
于是,我们可以将枚举法求t替换为二分法求t。
/*
1.从第一个机器人开始,依次让它们清扫各自范围内的走廊
2.对于所有的机器人都有一个固定清扫的最长时间,即所有机器人的清扫时间都不能超过这个数
3.因此我们真正的第一步应该是二分法枚举时间
4.然后在这个枚举的时间之下让机器人清扫地板
5.最后判断一下该地板是否全部被清扫完毕
6.那么我们该如何保证求出的是时间的最小值呢?
这实际上就是二分的性质了,当时间大于等于某一个临界值时,我们的区域一定可以被清扫完成,
因此此时我们需要找到的是满足这个条件的最小值,也就是靠近右边的线段上的最小值
*/
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
int n, k;
int a[N];
bool check(int mid) //接下来我们需要判断的是在mid时间内机器人是否能成功清扫完地板
{
//初始时没有一块地被清扫
int pos = 0; //表示的是1 ~ pos区域已经清扫完了,pos + 1 ~ n待清扫
//清扫的过程实际上是每次pos位置变化的过程
for(int i = 1; i <= k; i ++ ) //枚举所有的机器人
{
int t = mid;
//先向左清扫
if(pos < a[i]) t -= 2 * (a[i] - pos - 1);
if(t < 0) return false;
//再向右清扫
pos = a[i] + t / 2;
}
if(pos < n) return false;
return true;
}
int main()
{
scanf("%d%d", &n, &k);
for(int i = 1; i <= k; i ++ ) scanf("%d", &a[i]);
sort(a + 1, a + 1 + k); //将机器人的位置从小到大排个序
int l = 0, r = 2 * n, ans = 2 * n;
while(l < r)
{
int mid = l + r >> 1;
if(check(mid)) ans = mid, r = mid;
else l = mid + 1;
}
printf("%d\n", ans);
return 0;
}
五、123(枚举、前缀和、二分,2021)
题目描述
小蓝发现了一个有趣的数列,这个数列的前几项如下:
1,1,2,1,2,3,1,2,3,4,......
小蓝发现,这个数列前 1项是整数 1,接下来 2项是整数1 至 2,接下来 3项是整数 1至 3,接下来 4项是整数1至 4,依次类推。
小蓝想知道,这个数列中,连续一段的和是多少。
输入描述
输入的第一行包含一个整数 T,表示询问的个数。
接下来 T行,每行包含一组询问,其中第 i行包含两个整数 和 ,表示询问数列中第 个数到第 个数的和。
输出描述
输出 T行,每行包含一个整数表示对应询问的答案。
评测用例规模与约定
对于 10% 的评测用例,1 <= T <= 30,1 <= <= <= 100。 对于 20% 的评测用例, 1 <= T <= 100,1 <= <= <= 1000。 对于 40% 的评测用例,1 <= T <= 1000,1 <= <= <= 。 对于 70% 的评测用例, 1 <= T <= 10000,1 <= <= <= 。
对于 80% 的评测用例,1 <= T <= 1000,1 <= <= <= 。
对于 90% 的评测用例,1 <= T <= 10000,1 <= <= <= 。
对于所有评测用例,1 <= T <= 100000,1 <= <= <= 。
思路
1)20%做法(TLE)
题目要我们求出[L, R]的和。由于此时的数据范围比较小,所以我们可以按照数列变化的规律模拟出第l ~ r项的值,然后记录第l ~ r项的和就可以解决。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1000010;
int a[N];
int main()
{
int T;
cin >> T;
int up = 1, x = 1;
for(int i = 1; i <= 1000000; i ++ )
{
a[i] = x;
x ++;
if(x > up) up ++, x = 1; //模拟数组的变化规律
}
while(T --)
{
int l, r;
cin >> l >> r;
int sum = 0;
for(int i = l; i <= r; i ++ )
sum += a[i];
cout << sum << endl;
}
}
2)40%做法
前缀和优化:可以使用前缀和求区间“[L, R]”的和。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1000010;
int a[N], sum[N];
int main()
{
int T;
cin >> T;
int up = 1, x = 1;
for(int i = 1; i <= 1000000; i ++ )
{
a[i] = x;
x ++;
if(x > up) up ++, x = 1;
}
for(int i = 1; i <= 1000000; i ++ )
sum[i] = sum[i - 1] + a[i];
while(T --)
{
int l, r;
cin >> l >> r;
int s = sum[r] - sum[l - 1];
cout << s << endl;
}
}
3)因为本题的数据范围比较大,在有限的时间里面我们无法构造出数列的所有项,无法得到sum[]数组,因此我们需要重新考虑如何求解。
回到最开始,我们可以将数列分成若干组,其中第1组有1个数,第2组有2个数,...,第i组有i个数,那么前i组就公用1 + 2 + ... + i = 个数。
可以发现,使用不超过2 x 组,就能包含数列的前2 X 个数,因此,我们只需要关注前2 x 组。
我们知道,数列中的某一项必然属于某一组中的某一位置,那么不妨让我们假设数列的第x项位于第i组第j个位置。根据分组方式容易发现,sum[x] = a[1] + a[2] + ...... + a[x]可以用前i - 1组的所有数的和加上第i组的前j个数的和表示。
对于前i组所有数的和,我们用数组pre[]表示。
根据它的含义,可以轻松得到pre[i]的递推式:
pre[1] = 1;
pre[2] = pre[1] + (1 + 2);
pre[3] = pre[2] + (1 + 2 + 3);
……
pre[i] = pre[i - 1] + (1 + 2 + 3 + …… + i) = pre[i - 1] + 。
由于我们只需要考虑前2 X 个组,所以数组pre[]可以直接通过递推式线性求出。
在求出pre[]后,sum[x]就可以用pre[i - 1] + 1 + 2 + …… + j = pre[i - 1] + 表示了。
对于每组查询,其答案等于sum[R] - sum[L - 1]。所以,我们只需要分别求出L - 1,R在第几组、第几个位置,就可以求出sum[R]、sum[L - 1]了。
假设我们一直sum[R]在第i组的第j个位置,那么我们该如何求出i,j的具体取值?
已知在第i组之前,有i - 1个组,这些组共有1 + 2 + 3 + …… + (i - 1) = 个数。显然,随着i增大,的值就会越来越接近R,直到大于R。这说明i是满足单调性的。于是我们可以二分求解i。在直到i之后,j就可以进一步算出,即j = R - 。
#include <iostream>
#include <algorithm>
#include <cstring>
#define int long long
using namespace std;
const int N = 2e6 + 10;
int pre[N];
int calu(int x)
{
int l = 0, r = 2e6, i = 0;
while(l < r)
{
int mid = l + r >> 1;
if((mid + 1) * mid / 2 >= x) r = mid, i = mid;
else l = mid + 1;
}
int j = x - (i - 1) * (i - 1 + 1) / 2;
return pre[i - 1] + j * (j + 1) / 2;
}
signed main()
{
pre[1] = 1;
for(int i = 2; i <= 2000000; i ++ )
pre[i] = pre[i - 1] + i * (i + 1) / 2;
int T;
cin >> T;
while(T --)
{
int l, r;
cin >> l >> r;
cout << calu(r) - calu(l - 1) << endl;
}
return 0;
}