This commit is contained in:
HuangHai
2025-08-30 18:35:01 +08:00
commit 1f397eca87
6258 changed files with 1487241 additions and 0 deletions

View File

@@ -0,0 +1,282 @@
##[$AcWing$ $95$. 费解的开关](https://www.acwing.com/problem/content/97/)
### 一、题目描述
你玩过 **拉灯** 游戏吗?
$25$ 盏灯排成一个 $5×5$ 的方形。
每一个灯都有一个开关,游戏者可以改变它的状态。
**每一步,游戏者可以改变某一个灯的状态**。
游戏者改变一个灯的状态会产生连锁反应:**和这个灯上下左右相邻的灯也要相应地改变其状态**。
我们用数字 $1$ 表示一盏开着的灯,用数字 $0$ 表示关着的灯。
下面这种状态
```cpp {.line-numbers}
10111
01101
10111
10000
11011
```
在改变了最左上角的灯的状态后将变成:
```cpp {.line-numbers}
01111
11101
10111
10000
11011
```
再改变它正中间的灯后状态将变成:
```cpp {.line-numbers}
01111
11001
11001
10100
11011
```
给定一些游戏的初始状态,编写程序判断游戏者是否可能在 $6$ 步以内使所有的灯都变亮。
**输入格式**
第一行输入正整数 $n$,代表数据中共有 $n$ 个待解决的游戏初始状态。
以下若干行数据分为 $n$ 组,每组数据有 $5$ 行,每行 $5$ 个字符。
每组数据描述了一个游戏的初始状态。
各组数据间用一个空行分隔。
**输出格式**
一共输出 $n$ 行数据,每行有一个小于等于 $6$ 的整数,它表示对于输入数据中对应的游戏状态最少需要几步才能使所有灯变亮。
对于某一个游戏初始状态,若 $6$ 步以内无法使所有灯变亮,则输出 $−1$。
**数据范围**
$0<n≤500$
**输入样例:**
```cpp {.line-numbers}
3
00111
01011
10001
11010
11100
11101
11101
11110
11111
11111
01111
11111
11111
11111
11111
```
**输出样例:**
```cpp {.line-numbers}
3
2
-1
```
### 二、$bfs$解法
$bfs$可以解决这道题 时间复杂度:$2^{25}$
$bfs$的思路采用的是 **逆向思维法**
从终止状态倒推$6$步,先做一遍预处理。看看从终止状态可以从哪些状态在$6$步之内到达。
将状态用$hash$方法保存下来,$2^{25}$,最多是$3000W$左右个状态,但合法状态不是很多
><font color='red' size=3><b>解释:
为啥非得反着来,逆向思维?正着来,正向思维为啥就不行呢?
你想啊,因为最多输入$n=500$组数据,如果是正向思考的话,就是:
你给我一个现在的状态,我就想办法模拟它每个位置都可能变化,然后进行$bfs$拓展$6$次,一次是$2^{25}$,再乘个$6$,给我$500$组,我还需要再乘个$500$,常数有点大噢~
如果反着想呢?从结果出发,倒着思考,就是最终全是$1$,它的每个位置都可能变化,就是$25$个数位,枚举二进制来模拟每个变化的情况,变化$6$次记录到$Hash$ 表中,然后你给我一个状态,我就查一下表,查表的效率是$O(1)$的,明显比正着考虑节约了一个常数$500$啊,也就是性能提升了$500$倍。
逆向思维,小学奥数啊~
</b></font>
```cpp {.line-numbers}
#pragma GCC optimize(2) // 累了就吸点氧吧~
#include <bits/stdc++.h>
using namespace std;
// 5858 ms
// 当前状态,当前到最终状态所需步数
unordered_map<int, int> step;
// 改变这个灯及其上下左右相邻的灯的状态
int turn(int t, int idx) { // idx从0开始
t ^= (1 << idx); // 改变第idx个灯,0->1,1->0,互换状态,其它灯不动
if (idx % 5) t ^= (1 << idx - 1); // 不为最左一列,将左侧灯改变
if (idx >= 5) t ^= (1 << idx - 5); // 不为最上一排,将上侧灯改变
if (idx < 20) t ^= (1 << idx + 5); // 不为最后一排,将下面灯改变
if (idx % 5 < 4) t ^= (1 << idx + 1); // 不为最后一列,将右面灯改变
return t;
}
/* 我们需要知道某个局面是不是出现过,也就是需要把状态记录下来。
而一个二维的数组状态不好记录,我们喜欢记录一维的。
每个位置不是0,就是1,最多25个格子,联想到状态压缩,用二进制来表示状态。
从输入的状态开始,走6步,可以覆盖到哪些数组状态呢?我们可以用状态压缩+Hash表记录下来,最终判断一下
int x = (1 << 25) - 1;这个状态是不是在Hash表中就行了。
也可以使用逆向思维:从最终状态逆序遍历,找出可以在6步之内到达最终状态的所有可行状态,应该是一样的
*/
void bfs() {
// 0-2^25-1(25个1),共2^25种状态
// 最终的状态 全都是 `11111`
int x = (1 << 25) - 1; // 左移 右移的优先级是最低的,比加减还要低。所以这里的括号是必需的
queue<int> q;
q.push(x);
step[x] = 0; // 记录这个状态是否出现过,如果出现过,是走几步出现的
while (q.size()) {
auto t = q.front();
q.pop();
if (step[t] == 6) break; // 反向最多扩展6步
for (int i = 0; i < 25; i++) {
x = turn(t, i); // u:当前状态,i:改变i这一位置,x:将此状态改变后的新状态
if (!step.count(x)) { // 过滤过出现过的状态,只要没有出现过的状态
step[x] = step[t] + 1;
q.push(x);
}
}
}
}
int main() {
// 预处理
bfs();
int T;
scanf("%d", &T);
while (T--) {
int g = 0; // g是地图
for (int i = 0; i < 25; i++) {
char c;
// 注意在scanf读取完一个整数,再次读取char时,要清空缓冲区,用" %c"解决
scanf(" %c", &c);
g += ((c - '0') << i); // 25个字符二进制压缩成数字
}
if (step.count(g) == 0)
puts("-1");
else
cout << step[g] << endl;
}
return 0;
}
```
### 四、递推解法
* 二进制枚举+递推可以解决这道题 $2^5\times 25 \times 5 \times 500$
咋个递推法?递推嘛,顾名思义,就是一行一行的推!
比如现在给我们一个状态,我们先来看第一行,第一行共$5$个位置,每个位置是不是$0$就是$1$,每个位置都可能改变,改变的总数就是一个类似于二进制的所有组合情况,即$00000 \sim 11111$,这可不是真的让你把每个位置上的状态改变成上面的$00000 \sim 11111$,而是就$00000 \sim 11111$代表了每一种改变的办法,也就是哪个位置按下开关,哪个位置不按下开关,可别想错了。
按完会怎样呢?当然灯的状态会改变了!假设我们选择了一种按法,那么第一行的最终状态可能长成这个样子:
```
11001
```
啊?改变完了出现了两个$0$,这能行啊?最终的答案是要全都是$1$,它现在有两个$0$,是不是不合法,不应该这么按呢?不是啊,因为人家还没有最后收工按完,还继续按,你怎么知道人家以后按完会不会把这两个$0$变成$1$?人家要是可以变回$1$,那就是一个合法状态嘛。
那他能变回$1$吗?有什么办法能把这两个$0$变回$1$呢?仔细读题,我们知道,这时只有一个办法:就是在下一行的同列位置,也就是第$2$行的第$3$列,第$4$列再次按下开关,就能把第一行的两个$0$变成$1$!
也就是说,当我们枚举出第一行的每一种变化(共$32$种变化)后,第二行就不能随意再按了,因为比如
```cpp {.line-numbers}
11001
10111
...
...
...
```
第二行如果你按下了第一列,那么坏了,上面第一行第一列的$1$被你整没了!
第二行如果你按下了第二列,那么坏了,上面第一行第二列的$1$被你整没了!
而第二行第三列,第四列你就必须按下,要不上面第一行的两个$0$就没救了!
噢,我们似乎发现了什么规律:
当第一行确定了状态时,第二行的按法是唯一的,你想改都不行!
当你按唯一的办法按下第二时,我们来到第三行,此时发现,第三行的按法也是唯一的。
....
当我们来到边界的第五行时,它就不一样了!为什么呢?因为它没有后继了,它要是不全是$1$,后来也没人帮它改了,所以,它必须全是$1$,如果不是的话,那么第一行枚举到的这个状态就是不合法状态。
那还有一个$6$次修改的限制呢?这个放在哪里进行限定呢?
就是每枚举一个状态,在每行不断变化开关时,记录下开关的次数,如果最后第五行的每一个位置都是$1$,并且,次数最少,而且,少于等于$6$次的就是答案。
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int N = 6;
// char 数组版本
char g[N][N], bg[N][N]; // 工作的临时数组 和 原始状态数组
// 上右下左中
int dx[] = {-1, 0, 1, 0, 0};
int dy[] = {0, 1, 0, -1, 0};
// 按一下第x行第y列的开关
void turn(int x, int y) {
for (int i = 0; i < 5; i++) {
int tx = x + dx[i], ty = y + dy[i];
if (tx < 0 || tx >= 5 || ty < 0 || ty >= 5) continue;
g[tx][ty] ^= 1; //'0':48 '1':49
// (48)_{10}=(110000)_{2} (49)_{10}=(110001)_{2}
// 由于48的最后一位是0,而49最后一位是1,所以,异或就可以实现两者之间的切换,真是太神奇了~
}
}
int main() {
int T;
cin >> T;
while (T--) {
// 按一行的字符串读取
for (int i = 0; i < 5; i++) cin >> bg[i]; // 原来的字符状态数组
// 预求最小,先设最大
int res = INF;
// 枚举所有可能的操作办法:00000,00001,00010,...,11111,共32种办法
for (int op = 0; op < (2 << 5); op++) {
int cnt = 0;
memcpy(g, bg, sizeof g); // 将原始状态复制出一份放入g数组,开始尝试
// 操作办法op,共5位,1代表修改,0代表不改,按二进制由右到左可以枚举出这种操作需要改哪几列
for (int i = 0; i < 5; i++)
if (op >> i & 1) {
turn(0, i); // 你想改就改吧~
cnt++;
}
// 第二行需要观察第一行,啊~我同位的上一行有一个0!这可是大事,因为我必须补上它,否则就没有人可以补上它了~
for (int i = 0; i < 4; i++)
for (int j = 0; j < 5; j++)
if (g[i][j] == '0') {
turn(i + 1, j); // i+1行必须按下按钮
cnt++;
}
// 检查最后一行灯是否全亮.因为如果它还有0存在,就没机会补全了
bool success = true;
for (int i = 0; i < 5; i++)
if (g[4][i] == '0') success = false;
if (success) res = min(res, cnt); // 如果成功全亮,那你是多少步呢?
}
// 题意要求,大于6次,算失败
if (res > 6) res = -1;
cout << res << endl;
}
return 0;
}
```

View File

@@ -0,0 +1,66 @@
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int N = 6;
// char 数组版本
char g[N][N], bg[N][N]; // 工作的临时数组 和 原始状态数组
// 上右下左中
int dx[] = {-1, 0, 1, 0, 0};
int dy[] = {0, 1, 0, -1, 0};
// 按一下第x行第y列的开关
void turn(int x, int y) {
for (int i = 0; i < 5; i++) {
int tx = x + dx[i], ty = y + dy[i];
if (tx < 0 || tx >= 5 || ty < 0 || ty >= 5) continue;
g[tx][ty] ^= 1; //'0':48 '1':49
// (48)_{10}=(110000)_{2} (49)_{10}=(110001)_{2}
// 由于48的最后一位是0,而49最后一位是1,所以,异或就可以实现两者之间的切换,真是太神奇了~
}
}
int main() {
int T;
cin >> T;
while (T--) {
// 按一行的字符串读取
for (int i = 0; i < 5; i++) cin >> bg[i]; // 原来的字符状态数组
// 预求最小,先设最大
int res = INF;
// 枚举所有可能的操作办法:00000,00001,00010,...,11111,共32种办法
for (int op = 0; op < (2 << 5); op++) {
int cnt = 0;
memcpy(g, bg, sizeof g); // 将原始状态复制出一份放入g数组,开始尝试
// 操作办法op,共5位,1代表修改,0代表不改,按二进制由右到左可以枚举出这种操作需要改哪几列
for (int i = 0; i < 5; i++)
if (op >> i & 1) {
turn(0, i); // 你想改就改吧~
cnt++;
}
// 第二行需要观察第一行,啊~我同位的上一行有一个0!这可是大事,因为我必须补上它,否则就没有人可以补上它了~
for (int i = 0; i < 4; i++)
for (int j = 0; j < 5; j++)
if (g[i][j] == '0') {
turn(i + 1, j); // i+1行必须按下按钮
cnt++;
}
// 检查最后一行灯是否全亮.因为如果它还有0存在,就没机会补全了
bool success = true;
for (int i = 0; i < 5; i++)
if (g[4][i] == '0') success = false;
if (success) res = min(res, cnt); // 如果成功全亮,那你是多少步呢?
}
// 题意要求,大于6次,算失败
if (res > 6) res = -1;
cout << res << endl;
}
return 0;
}

View File

@@ -0,0 +1,67 @@
#pragma GCC optimize(2) // 累了就吸点氧吧~
#include <bits/stdc++.h>
using namespace std;
// 5858 ms
// 当前状态,当前到最终状态所需步数
unordered_map<int, int> step;
// 改变这个灯及其上下左右相邻的灯的状态
int turn(int t, int idx) { // idx从0开始
t ^= (1 << idx); // 改变第idx个灯,0->1,1->0,互换状态,其它灯不动
if (idx % 5) t ^= (1 << idx - 1); // 不为最左一列,将左侧灯改变
if (idx >= 5) t ^= (1 << idx - 5); // 不为最上一排,将上侧灯改变
if (idx < 20) t ^= (1 << idx + 5); // 不为最后一排,将下面灯改变
if (idx % 5 < 4) t ^= (1 << idx + 1); // 不为最后一列,将右面灯改变
return t;
}
/* 我们需要知道某个局面是不是出现过,也就是需要把状态记录下来。
而一个二维的数组状态不好记录,我们喜欢记录一维的。
每个位置不是0,就是1,最多25个格子,联想到状态压缩,用二进制来表示状态。
从输入的状态开始,走6步,可以覆盖到哪些数组状态呢?我们可以用状态压缩+Hash表记录下来,最终判断一下
int x = (1 << 25) - 1;这个状态是不是在Hash表中就行了。
也可以使用逆向思维:从最终状态逆序遍历,找出可以在6步之内到达最终状态的所有可行状态,应该是一样的
*/
void bfs() {
// 0-2^25-1(25个1),共2^25种状态
// 最终的状态 全都是 `11111`
int x = (1 << 25) - 1; // 左移 右移的优先级是最低的,比加减还要低。所以这里的括号是必需的
queue<int> q;
q.push(x);
step[x] = 0; // 记录这个状态是否出现过,如果出现过,是走几步出现的
while (q.size()) {
auto t = q.front();
q.pop();
if (step[t] == 6) break; // 反向最多扩展6步
for (int i = 0; i < 25; i++) {
x = turn(t, i); // u:当前状态,i:改变i这一位置,x:将此状态改变后的新状态
if (!step.count(x)) { // 过滤过出现过的状态,只要没有出现过的状态
step[x] = step[t] + 1;
q.push(x);
}
}
}
}
int main() {
// 预处理
bfs();
int T;
scanf("%d", &T);
while (T--) {
int g = 0; // g是地图
for (int i = 0; i < 25; i++) {
char c;
// 注意在scanf读取完一个整数,再次读取char时,要清空缓冲区,用" %c"解决
scanf(" %c", &c);
g += ((c - '0') << i); // 25个字符二进制压缩成数字
}
if (step.count(g) == 0)
puts("-1");
else
cout << step[g] << endl;
}
return 0;
}

View File

@@ -0,0 +1,227 @@
##[$AcWing$ $97$. 约数之和](https://www.acwing.com/problem/content/description/99/)
### 一、题目描述
假设现在有两个自然数 $A$ 和 $B$,$S$ 是 $A^B$ 的所有约数之和。
请你求出 $S~ mod ~9901$ 的值是多少。
**输入格式**
在一行中输入用空格隔开的两个整数 $A$ 和 $B$。
**输出格式**
输出一个整数,代表 $S~ mod~ 9901$ 的值。
**数据范围**
$0≤A,B≤5×10^7$
**输入样例:**
```cpp {.line-numbers}
2 3
```
**输出样例:**
```cpp {.line-numbers}
15
```
注意: $A$ 和 $B$ 不会同时为 $0$。
### 二、理论知识
#### 唯一分解
$$\large N=p_1^{\alpha_1}\cdot p_2^{\alpha_2}\cdot ... \cdot p_k^{\alpha_k}$$
#### 约数个数
$$\large (\alpha_1+1)(\alpha_2+1)...(\alpha_k+1)$$
**证明**:因为$\large p_1^{\alpha_1}$的约数有$\large p_1^0,p_1^1,p_1^2,...,p_1^{\alpha_1}$,共$\large a_1+1$个,同理$p_k^{\alpha_k}$的约数有$\large a_k+1$个,根据乘法原理,就知道了约数的总个数。
#### 约数和
$$\large (1+p_1^1+p_1^2+...+p_1^{\alpha_1})(1+p_2^1+p_2^2+...+p_2^{\alpha_2})...(1+p_n^1+p_n^2+...+p_n^{\alpha_n})$$
**证明**:$p_1^{a_1}$ 的约数有$p_1^{0},p_1^{1},p_1^{2}...p_1^{a_1}$共 $a_1+1$个,$p_2^{a_2}...p_k^{a_k}$ 同理,$n$的一个约数就是在$p_1^{a_1},p_2^{a_2},p_3^{a_3}...p_k^{a_k}$ 每一个的约数中任挑一个组成的,挑法的个数就是约数个数,根据乘法原理他们的和为$(p_1^0+p_1^1+...+p_1^{a_1})(p_2^0+p_2^1+...+p_2^{a_2})...(p_k^0+p_k^1+...+p_k^{a_k})$
**练习题**
[约数之和](https://www.acwing.com/problem/content/description/873/)
[分解质因数](https://www.acwing.com/problem/content/869/)
#### 举栗子: $180=2^2∗3^2∗5^1$
约数个数:$(2+1)(2+1)(1+1)=18$
约数和:$(1+2+4)(1+3+9)(1+5)=546$
### 三、分治法求解等比数列求和
> **巧妙的数学变换**
令$\large sum(p, k)=p^0+p^1+…+p^{k−1}$
* **$k$为偶数**
$$\large sum(p,k)=p^0+p^1+…+p^{k/2−1}+p^{k/2}+p^{k/2+1}+…+p^{k−1} \\
\Rightarrow \\
(p^0+p^1+…+p^{k/2−1})+p^{k/2}∗(p^0+p^1+…+p^{k/2−1}) \\
\Rightarrow \\
sum(p,k/2)+p^{k/2}∗sum(p,k/2) \\
\Rightarrow \\
(p^{k/2}+1)∗sum(p,k/2)
$$
><font color='red' size=4><b>解释:这样我们就得到了一个递推式,
>- $p^{k/2}$ 可以用快速幂计算
>- $sum(p,k/2)$ 由于我们的递推是从小到大推的,在推$sum(p,k)$时,$sum(p,k/2)$已
经算完了,可以直接用。
</b></font>
* **$k$为奇数**
为了更方便调用我们写的偶数项情况,可以单独拿出最后一项,把剩下的项转化为求偶数项的情况来考虑,再加上最后一项,就是奇数项的情况了。也即
$$\large sum(p,k)=p^0+p^1+p^2+...+p^{k-1} \\
= sum(p,k-1)+p^{k-1} $$
当然,也可以拿出来第一项,就是
$$\large sum(p,k)=p^0+p^1+p^2+...+p^{k-1} \\
= p^0+p\times (p^0+p^1+...+p^{k-2}) \\
= 1+ p \times sum(p,k-1)
$$
下面的代码实现中,我将使用拿出最后一项的办法:
```cpp {.line-numbers}
int sum(int p, int k) {
if(k == 1) return 1; //边界
if(k % 2 == 0)
return (LL)(qmi(p, k / 2) + 1) * sum(p, k / 2) % mod;
return (qmi(p, k - 1) + sum(p, k - 1)) % mod;
}
```
**实现代码**
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int mod = 9901;
int A, B;
// 分解质因数
unordered_map<int, int> primes; // map存的是数对,key+value,默认按key由小到大排序
void divide(int x) {
for (int i = 2; i * i <= x; i++)
while (x % i == 0) primes[i]++, x /= i;
if (x > 1) primes[x]++;
}
// 快速幂
int qmi(int a, int b) {
int res = 1;
while (b) { // 对b进行二进制分解(我给起的名),枚举b的每一个二进制位
if (b & 1) res = (LL)res * a % mod;
a = (LL)a * a % mod;
b >>= 1;
}
return res;
}
// 分治法求等比数列求和公式
int sum(int p, int k) {
if (k == 1) return 1; // 递归出口
if (k % 2 == 0) // 偶数
return (LL)(qmi(p, k / 2) + 1) * sum(p, k / 2) % mod;
return (qmi(p, k - 1) + sum(p, k - 1)) % mod; // 奇数
}
int main() {
cin >> A >> B;
// 对A分解质因子
divide(A);
int res = 1;
for (auto it : primes) {
// p是质因子,k是质因子的次数
int p = it.first, k = it.second * B; // 约数和公式,需要到每个质因子的it.second次方*B
// res要乘上每一项, 注意这里是k + 1
res = (LL)res * sum(p, k + 1) % mod; // 满满的套路感
}
if (!A) res = 0; // 还要特判A是不是0,注意思考数据边界
cout << res << endl;
return 0;
}
```
时间复杂度 $O(\sqrt{n}lognlogn)$
### 四、等比数列求和公式
$$\large p^0+p^1+p^2+...+p^{k-1}=\frac{p^{k}-1}{p-1}$$
除了分治法外,还可以用 **乘等比数列和公式** 得出分子,再 **乘分母逆元** 的做法:
**快速幂求逆元**
这样的话,就可以愉快地套用高中学的等比数列和公式来求每一项,但这里需要 **特判逆元不存在** 的情况:
* 分母$p-1$是$mod$的倍数,不存在逆元,这时直接乘$(1+p^1+…p^k)\% mod$, 即 $k+1$
* 分母$p-1$不是$mod$的倍数,存在 **逆元**,这是需要用 **快速幂求分子**,再用 **快速幂求分母的逆元**,两者相乘就得到了每一项
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int mod = 9901;
int A, B;
//分解质因数
map<int, int> primes; // map存的是数对,key+value,默认按key由小到大排序
void divide(int x) {
for (int i = 2; i <= x / i; i++)
while (x % i == 0) primes[i]++, x /= i;
if (x > 1) primes[x]++;
}
//快速幂
int qmi(int a, int b) {
int res = 1;
while (b) {
if (b & 1) res = (LL)res * a % mod;
a = (LL)a * a % mod;
b >>= 1;
}
return res;
}
int main() {
scanf("%d %d", &A, &B);
//对A分解质因子
divide(A);
int res = 1;
for (auto it : primes) {
// p是质因子,k是质因子的次数
int p = it.first, k = it.second * B;
// res要乘上每一项, 注意这里是k + 1
if ((p - 1) % mod == 0) {
//不存在逆元,由于p-1的是mod的倍数, 故p%mod=1
//所以1 + p + ... + p^k每个数%mod都是1,共k + 1个数,总就是k + 1
res = (LL)res * (k + 1) % mod;
} else
//分子用快速幂计算,注意标准公式和此题的区别,k+1
//分母用费马小定理求逆元 qmi(p-1,mod-2)
res = (LL)res * (qmi(p, k + 1) - 1) % mod * qmi(p - 1, mod - 2) % mod;
}
if (!A) res = 0;
printf("%d\n", (res % mod + mod) % mod);
return 0;
}
```
时间复杂度$O(\sqrt{n}logn)$

View File

@@ -0,0 +1,51 @@
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int mod = 9901;
int A, B;
// 分解质因数
unordered_map<int, int> primes; // map存的是数对,key+value,默认按key由小到大排序
void divide(int x) {
for (int i = 2; i * i <= x; i++)
while (x % i == 0) primes[i]++, x /= i;
if (x > 1) primes[x]++;
}
// 快速幂
int qmi(int a, int b) {
int res = 1;
while (b) { // 对b进行二进制分解(我给起的名),枚举b的每一个二进制位
if (b & 1) res = (LL)res * a % mod;
a = (LL)a * a % mod;
b >>= 1;
}
return res;
}
// 分治法求等比数列求和公式
int sum(int p, int k) {
if (k == 1) return 1; // 递归出口
if (k % 2 == 0) // 偶数
return (LL)(qmi(p, k / 2) + 1) * sum(p, k / 2) % mod;
return (qmi(p, k - 1) + sum(p, k - 1)) % mod; // 奇数
}
int main() {
cin >> A >> B;
// 对A分解质因子
divide(A);
int res = 1;
for (auto it : primes) {
// p是质因子,k是质因子的次数
int p = it.first, k = it.second * B; // 约数和公式,需要到每个质因子的it.second次方*B
// res要乘上每一项, 注意这里是k + 1
res = (LL)res * sum(p, k + 1) % mod; // 满满的套路感
}
if (!A) res = 0; // 还要特判A是不是0,注意思考数据边界
cout << res << endl;
return 0;
}

View File

@@ -0,0 +1,49 @@
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int mod = 9901;
int A, B;
//分解质因数
map<int, int> primes; // map存的是数对,key+value,默认按key由小到大排序
void divide(int x) {
for (int i = 2; i <= x / i; i++)
while (x % i == 0) primes[i]++, x /= i;
if (x > 1) primes[x]++;
}
//快速幂
int qmi(int a, int b) {
int res = 1;
while (b) {
if (b & 1) res = (LL)res * a % mod;
a = (LL)a * a % mod;
b >>= 1;
}
return res;
}
int main() {
scanf("%d %d", &A, &B);
//对A分解质因子
divide(A);
int res = 1;
for (auto it : primes) {
// p是质因子,k是质因子的次数
int p = it.first, k = it.second * B;
// res要乘上每一项, 注意这里是k + 1
if ((p - 1) % mod == 0) {
//不存在逆元,由于p-1的是mod的倍数, 故p%mod=1
//所以1 + p + ... + p^k每个数%mod都是1,共k + 1个数,总就是k + 1
res = (LL)res * (k + 1) % mod;
} else
//分子用快速幂计算,注意标准公式和此题的区别,k+1
//分母用费马小定理求逆元 qmi(p-1,mod-2)
res = (LL)res * (qmi(p, k + 1) - 1) % mod * qmi(p - 1, mod - 2) % mod;
}
if (!A) res = 0;
printf("%d\n", (res % mod + mod) % mod);
return 0;
}

View File

@@ -0,0 +1,37 @@
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
struct Point {
LL x, y;
};
// 功能:在一个n级的矩阵中,求标号为a的格子坐标
Point get(LL n, LL a) {
if (n == 0) return {0, 0}; // 1级是4个,2级是16个,3级是64个,0级时只有一个点,坐标就是(0,0)
LL block = 1ll << n * 2 - 2; // 1个块的大小:1级1个,2级4个,3级16个,也就是4^(n-1)=2^(2n-2)
LL len = 1ll << n - 1; // 1级移动1,2级移动2,3级移动4,总结:n级2^(n-1)
auto p = get(n - 1, a % block); // n-1级时,a这个点的原来对应格子在坐标是什么
LL x = p.x, y = p.y;
int z = a / block; // 0~3哪个块
if (z == 0) return {y, x}; // y=x对称
if (z == 1) return {x, y + len}; // 向右平移len个距离
if (z == 2) return {x + len, y + len}; // 向右平移len个距离,再向下移动len个距离
if (z == 3) return {len * 2 - 1 - y, len - 1 - x}; // 关于2^(n-1)-1那条直线对称
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
LL n, a, b;
scanf("%lld%lld%lld", &n, &a, &b);
auto pa = get(n, a - 1); // 下标从0开始,方便取模
auto pb = get(n, b - 1);
double dx = pa.x - pb.x, dy = pa.y - pb.y;
printf("%.0lf\n", sqrt(dx * dx + dy * dy) * 10);
}
return 0;
}

View File

@@ -0,0 +1 @@
<mxfile host="Electron" modified="2022-06-27T01:19:58.985Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/16.0.2 Chrome/96.0.4664.55 Electron/16.0.5 Safari/537.36" etag="dUMlzbT127kNW8z2ADPt" version="16.0.2" type="device"><diagram id="lXwLDaeAN21MMdg__sbH" name="第 1 页">5Zhdb9sgFIZ/jaXtIpUNthNfNmm3TvvQpGrd1DtmiI2GTYRJ4+zXD2qwg52sSZSl6noVeIFzzHkO9iEenBX1e4EW+WeOCfOAj2sPXnkABIEP1I9W1o0yGUeNkAmKzaROuKW/iRF9oy4pJpUzUXLOJF24YsrLkqTS0ZAQfOVOm3Pmel2gjAyE2xSxofqdYpkbNY7CbuCG0Cy3roM4aUYKZGebrVQ5wny1IcFrD84E57JpFfWMMB09G5hm3bsdo+2TCVLKfRaMs/rTx2+guL//kXyBH1Z1mLGRsfKA2NLs2DysXNsQkBJf6kiqXspQVdHUg9NcFkwJgWoKviwx0U581WuWEzwIa/eYQbt5lTaEF0SKtZqy6uIbmZDlG5G1miAMSfrgmkcGc9aaaz185VQ5Br5NSWvHJCQIfddExZciJWbVZjh7hsbxE4YkEhmRA0OqsbHtTnqkdQA58NrIhb2Ah5MjyfUNgfOCgweBK3lJXjI1mAQuNRgcRy2CPUPBWalFJ6WGRGq+deAlQFTUnNgHMTgOYhy7hmAAzkoxHlB8U3tgtn47gClJLV2ClRT8F5lxxkVHeE4Z60mI0azUr1zFjih9+kCEpKqkuDQDBcVYu5mucirJ7QKl2udKFVCDPJnzUtpEidtE0QZJfWiqtOfIffvZ7kYmxVsyCfq7k8ahdCiS8RCJMjob1S+BSXQaJnHSYxI8M5TJEMro1VGBoFdwbDkqyTmpJDuovCYo7c3Qfoqe+/1ln+dvlcEmBnsL1PHBqMq7mmDvwCsbC225qDN9675oLrrg4mdXgUwfL9yBDvrjZRxOVKvkMs3bCkTNuyFIe48ih1xTrkhVMHDtNvF7ieIBteR6chWehmiYRC7RZEh0soXo+J8R3eNS/P8SxVSQ1EwsudAEpnNGFzf2TKv2nbF5ipds8vR5Hm+h37/87UFfdbt/W5qKsvvTCl7/AQ==</diagram></mxfile>

View File

@@ -0,0 +1,106 @@
##[$AcWing$ $98$. 分形之城](https://www.acwing.com/problem/content/description/100/)
### 一、题目描述
城市的规划在城市建设中是个大问题。
不幸的是,很多城市在开始建设的时候并没有很好的规划,城市规模扩大之后规划不合理的问题就开始显现。
而这座名为 $Fractal$ 的城市设想了这样的一个规划方案,如下图所示:
<center><img src='https://www.acwing.com/media/article/image/2019/01/07/19_1ac25efe11-city.png'></center>
当城区规模扩大之后,$Fractal$ 的解决方案是把和原来城区结构一样的区域按照图中的方式建设在城市周围,提升城市的等级。
对于任意等级的城市,我们把正方形街区从左上角开始按照道路标号。
虽然这个方案很烂,$Fractal$ 规划部门的人员还是想知道,如果城市发展到了等级 $N$,编号为 $A$ 和 $B$ 的两个街区的 **直线距离** 是多少。
街区的距离指的是街区的 **中心点之间的距离**,每个街区都是边长为 $10$ 米的正方形。
**输入格式**
第一行输入正整数 $n$,表示测试数据的数目。
以下 $n$ 行,输入 $n$ 组测试数据,每组一行。
每组数据包括三个整数 $N,A,B$,表示城市等级以及两个街区的编号,整数之间用空格隔开。
**输出格式**
一共输出 $n$ 行数据,每行对应一组测试数据的输出结果,结果四舍五入到整数。
**数据范围**
$1≤N≤31,1≤A,B≤2^{2N},1≤n≤1000$
**输入样例:**
```cpp {.line-numbers}
3
1 1 2
2 16 1
3 4 33
```
**输出样例:**
```cpp {.line-numbers}
10
30
50
```
### 二、解题思路
我们假设想求$9$号点的坐标,当然,我们可以用眼睛看~,但是如果层级太多,眼睛看着太累,而且,计算也不会用眼睛看,我们给它一个办法:
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202310261117836.png)
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202310261115004.png)
是这样思考问题的:
$9$号坐标我是不知道的,但是,我们知道,$9$号在自己的小区间,也就是$4$方格子中,它是最小的,而且,等级$2$是由等级$1$变换而来,$1$这个数字,它经过一顿坐标变换,可以到达$9$, 如果我们知道了$1$的坐标$(x,y)$,再同样经过那些坐标变换过程,是不是就能求出$9$的坐标了?所以,这是一个大问题向小问题转化的过程,也就是$N$级的问题,我们可以先求$N-1$级的原来对应点的坐标,再通过对应点的坐标经过同样的变换,就得到了现在的坐标。
那$9$的原来坐标点是$1$,$10$的原来坐标点是$2$,$11$的原来坐标点是$3$,$12$的原来坐标点是$4$,噢,似乎是与数字$4$有取模的关系!既然要取模,那么最好下标从$0$开始,所以,我们不再说$1\sim 16$,改成$0 \sim 15$吧!
那就是$8:0,9:1,10:2,11:3$,也就是$8\%4=0,8\%4=1,10\%4=2,11\%4=3$
等一下,这个$4$是啥东西?常量吗?应该不是,似乎是等级$2$中整体数字数量$4^2/4=4$
推广到$n$级,就是$block=4^n/4=2^{2n-2}$
下面就是如何进行坐标变换了:
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202310261104362.jpg)
$Code$
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
struct Point {
LL x, y;
};
// 功能:在一个n级的矩阵中,求标号为a的格子坐标
Point get(LL n, LL a) {
if (n == 0) return {0, 0}; // 1级是4个,2级是16个,3级是64个,0级时只有一个点,坐标就是(0,0)
LL block = 1ll << n * 2 - 2; // 1个块的大小:1级1个,2级4个,3级16个,也就是4^(n-1)=2^(2n-2)
LL len = 1ll << n - 1; // 1级移动1,2级移动2,3级移动4,总结:n级2^(n-1)
auto p = get(n - 1, a % block); // n-1级时,a这个点的原来对应格子在坐标是什么
LL x = p.x, y = p.y;
int z = a / block; // 0~3哪个块
if (z == 0) return {y, x}; // y=x对称
if (z == 1) return {x, y + len}; // 向右平移len个距离
if (z == 2) return {x + len, y + len}; // 向右平移len个距离,再向下移动len个距离
if (z == 3) return {len * 2 - 1 - y, len - 1 - x}; // 关于2^(n-1)-1那条直线对称
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
LL n, a, b;
scanf("%lld%lld%lld", &n, &a, &b);
auto pa = get(n, a - 1); // 下标从0开始,方便取模
auto pb = get(n, b - 1);
double dx = pa.x - pb.x, dy = pa.y - pb.y;
printf("%.0lf\n", sqrt(dx * dx + dy * dy) * 10);
}
return 0;
}
```

Binary file not shown.