0%

那我问你,你做过你牛叠的雷霆大DP吗?

牛客137448F-猴子排序的剪枝

题意

给定一个长度为 $n$ 的数组,每次随机抽取一个还没被选择的元素放入新数组末尾。如果出现前一个大于后一个,则清空新数组重新开始。求成功排序的期望操作次数。(可以证明答案可以表示为一个不可约分数 $\tfrac{p}{q}$,为了避免精度问题,请直接输出整数$ \left(p \times q^{-1} \bmod M\right)$作为答案,其中 M = 998244353)

第一行输入一个整数 n (1 ≤ n ≤ 5000)
第二行输入 n 个整数 $a_1,a_2,\ldots,a_n \left( 1 \le a_i \le 10^9\right)$。

样例1:

1
2
3
4
5
6
输入
2
1 2

输出
4

样例2:

1
2
3
4
5
6
输入
3
1 1 2

输出
8

AC码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N = 5005;
const int MOD = 998244353;
int fact[N], invfact[N];

int qmi(int a, int b) {
int res = 1;
while (b) {
if (b & 1) res = res * a % MOD;
b >>= 1LL;
a = a * a % MOD;
}
return res;
}

int inv(int x) {
return qmi(x, MOD - 2);
}

void solve(){

int n; cin >> n;
map<int, int> mp;
for (int i = 1; i <= n; ++i) {
int x; cin >> x;
++mp[x];
}

vector<vector<int>> dp(n + 1, vector<int> (n + 1));
dp[0][0] = 1;
int i = 0, m = 0;

// 最重要的一条:
for (auto [val, c]: mp) {
++i;
for (int j = 0; j <= m; ++j) {
if (!dp[i - 1][j]) continue; // 注意这里别写错成dp[i][j]了,错了好几次。
for (int k = 0; k <= c; ++k) {
dp[i][j + k] = (dp[i][j + k] + (dp[i - 1][j] * invfact[k]) % MOD) % MOD;
}
}
m += c;
}

int ans = 0;
for (int l = 1; l <= n; ++l) {
ans = (ans + dp[i][l] * fact[l] % MOD) % MOD;
}
cout << ans << '\n';

}

signed main(){

ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);

fact[0] = 1;
for (int i = 1; i <= 5000; ++i) {
fact[i] = fact[i - 1] * i % MOD;
}
invfact[5000] = inv(fact[5000]);
for (int i = 4999; i >= 0; --i) {
invfact[i] = invfact[i + 1] * (i + 1) % MOD;
}

int _ = 1;
// cin >> _;
while (_--)
solve();
return 0;

}

先说样例是怎么来的:

示例 1 分析 (输入 1 2)

设从头开始直到排序成功的总期望操作数为 $E$。

  • 情况 A (50% 概率):先选 1,再选 2

    操作 2 次,没有出现前一个元素大于后一个元素的情况。排序成功,结束。

    这部分的代价是:$2$

  • 情况 B (50% 概率):先选 2,再选 1

    插入 2(1次操作),再插入 1(第2次操作)。此时新数组是 [2, 1],满足 $2 > 1$,本轮排序失败。

    此时你已经消耗了 2 次操作,并且一切从头开始。既然从头开始,后续还需要操作的期望次数依然是 $E$。

    这部分的代价是:$2 + E$

把它们加起来得到方程:

解这个方程:

这就是 $4$ 的由来。

示例 2 分析 (输入 1 1 2)

我们再推一下示例 2。全排列有 $A_3^3 = 6$ 种抽法,设总期望为 $E$:

  1. 抽到 1 1 2(2种情况,概率 $2/6 = 1/3$):

    操作 3 次成功。代价:$3$

  2. 抽到 1 2 1(2种情况,概率 $1/3$):

    前 2 次插入 12 是合法的,第 3 次插入 1 时发现 $2 > 1$,失败重来。消耗 3 次操作。

    代价:$3 + E$

  3. 抽到 2 1 1(2种情况,概率 $1/3$):

    第 1 次插入 2 合法,第 2 次插入 1 时发现 $2 > 1$,立刻失败重来。仅仅消耗 2 次操作。

    代价:$2 + E$

列出方程:

核心结论

对于这种“失败即重来”的模型,有一个通用的结论:

即 $E_{总} = \frac{E_{单轮操作}}{P_{单轮成功}}$,那么这题的本质就是一个推导并化简概率公式的数学题,最后用动态规划(或者说多项式乘法)来实现。

(注: 单轮操作步数 ($X$): 这是指在某局游戏里(被清空之前),你一共塞了几个元素进去。比如你塞了 2 然后塞 1,挂了,这一局的 $X = 2$。 )

1. 求分母:单轮成功的概率

假设原数组长度为 $n$,共有 $m$ 种不同的数字,第 $j$ 种数字出现的次数为 $c_j$。

成功概率 $P_n$:

一轮能成功,意味着抽出的 $n$ 个元素必须是不下降序列。也就是相同的数字必须挨在一起。

2.求分子:单轮期望操作数

一轮会在什么时候停止?要么失败停止,要么排满 $n$ 个成功停止。根据期望的线性性质(或者叫期望的尾部公式),操作 $X$ 次后结束的期望为:

这里如何理解?概率是怎么变成期望(次数)的?

我们要算的是“期望操作次数”。所以它的原始定义一定是:

我们把原始定义的公式拆开:

$E = 1 \times P(\text{恰好1次结束})$

$\quad\; + 2 \times P(\text{恰好2次结束})$

$\quad\; + 3 \times P(\text{恰好3次结束})$

$\quad\; + 4 \times P(\text{恰好4次结束})$

$\quad\; + \dots$

我们把它写成加法,排成一个三角形阵列

原本我们是横着一行一行加的。

如果现在我们竖着一列一列加

  • 第 1 列:$P(\text{恰好1次}) + P(\text{恰好2次}) + P(\text{恰好3次}) + \dots$

    所有的概率全加起来,等于多少?等于 $100\%$ ($1$)!因为游戏总要在某一次结束,也就是必定能跨过 $0$ 步,迈出第 $1$ 步。即 $P(X > 0)$。

  • 第 2 列:$P(\text{恰好2次}) + P(\text{恰好3次}) + P(\text{恰好4次}) + \dots$

    这是什么意思?意思是游戏在第 $1$ 次没有结束,至少撑到了第 2 次及以上。这不就是 $P(\text{前 1 个元素合法})$ 吗!也就是 $P(X > 1)$。

  • 第 3 列:$P(\text{恰好3次}) + P(\text{恰好4次}) + \dots$

    意思是游戏至少撑到了第 3 次及以上,即 $P(\text{前 2 个元素合法})$,也就是 $P(X > 2)$。

所以,把每一列的和加起来,公式就变成了:

即:

等价于:

设前 $k$ 个元素合法的概率为 $P_k$。要让前 $k$ 个元素合法,我们需要从第 $j$ 种数字中抽出 $x_j$ 个,满足 $\sum x_j = k$,且 $0 \le x_j \le c_j$。

抽出这些数字并排列合法的概率是:

怎么求的?

核心思想就是最基础的古典概型公式:

1. 分母:总的抽取方案数

从 $n$ 个元素中,依次抽出 $k$ 个元素。

因为我们把所有元素都看作不同的个体,并且是有顺序地一个个抽,这本质上是一个排列问题。

总方案数为从 $n$ 中取 $k$ 的排列数:$A_n^k = \frac{n!}{(n-k)!}$

2. 分子:合法的抽取方案数

什么样的抽取方案是“合法”的?不下降

既然要求不下降,意味着这 $k$ 个元素被抽出来后,它们的相对位置已经被彻底锁死了。所有的 1 必须排在所有的 2 前面,所有的 2 必须排在所有的 3 前面,依此类推。

假设在这 $k$ 个位置中,我们决定放入 $x_1$ 个第一种数字、$x_2$ 个第二种数字…… $x_m$ 个第 $m$ 种数字($x$ 可以取0)。

对于第 $j$ 种数字:

从 $c_j$ 个数字中抽出 $x_j$ 个并排列的方案数为:$A_{c_j}^{x_j} = \frac{c_j!}{(c_j - x_j)!}$

因为每种数字的抽取是互相独立的,根据乘法原理,对于一种特定的数量分配 $(x_1, x_2, \dots, x_m)$,合法的抽取方案数为:

这只是其中一种数量分配的方案数。由于我们可以在满足 $\sum x_j = k$ 的前提下,任意分配这 $k$ 个名额,所以需要把所有可能的分配情况累加起来。

这就是求和符号的来源:

合并公式

现在我们把分子和分母组合起来:

把分母 $A_n^k$ 替换为 $\frac{n!}{(n-k)!}$ ,得到了 $P_k$ 最终公式:

3. 化简

现在我们把总期望公式写出来:

把 $P_k$ 和 $P_n$ 代入,$ 会被约掉:

化简后只剩:

变量代换

设 $y_j = c_j - x_j$。既然 $\sum x_j = k$,那么 $\sum y_j = \sum c_j - \sum x_j = n - k$。

再设 $l = n - k$。当 $k$ 从 $0$ 遍历到 $n-1$ 时,$l$ 正好从 $n$ 遍历到 $1$。

公式变成:

所以这题只和数组大小 $n$,和数的种数量 $m$ 有关。

4. 转化为DP / 多项式思路(仙术)

仔细看后面这一坨:$\sum_{\sum y_j = l} \prod_{j=1}^m \frac{1}{y_j!}$

这不就是多项式乘法展开后的系数吗?!所以,我们为了让“想算的式子”变成“多项式乘法”,要怎么做? 直接让多项式里的系数 $A_y$ 和 $B_y$,等于 $\frac{1}{y!}$ 不就行了!

它完全等价于:将 $m$ 个多项式 $A_j(x) = \sum_{y=0}^{c_j} \frac{1}{y!} x^y$ 乘起来,结果中 $x^l$ 的系数!

这里 前面的系数 $\frac{1}{y!}$ 才是本体,它是我们真正需要相乘并累加的数值。 把它们绑定在一起写成 $\frac{1}{y!} x^y$,是为了白嫖“多项式乘法合并同类项”的规则,让计算机用两层 for 循环就能搞定原本极其复杂的全排列求和。

因为题目 $N \le 5000$,$O(N^2)$ 的算法完全可以跑过。我们根本不需要上 NTT,直接用背包 DP 的思路做多项式乘法即可。

代码实现框架 (C++):

  1. 统计每个数字出现的频次 $c_j$。
  2. 预处理阶乘及其逆元(模 $998244353$)。
  3. dp[i] 表示多项式乘积中 $x^i$ 的系数。初始 dp[0] = 1
  4. 遍历每一个频次 $c$:
    • dp[i][j + k] = (dp[i][j + k] + dp[i - 1][j] * invFact[k]) % MOD;
  5. 最终答案就是 $\sum_{l=1}^n (l! \times dp[m][l]) \pmod{998244353}$。

关键代码:

1
2
3
4
5
6
7
8
9
10
for (auto [val, c]: mp) {
++i;
for (int j = 0; j <= m; ++j) {
if (!dp[i - 1][j]) continue;
for (int k = 0; k <= c; ++k) {
dp[i][j + k] = (dp[i][j + k] + (dp[i - 1][j] * invfact[k]) % MOD) % MOD;
}
}
m += c;
}

这题总算结束了,哦对了。

CF2167F

题意

  • 给定一棵树和参数 k
  • 定义集合$S_r$:以点 r 作为根,考虑该树的每一组 k 个不同节点得到的LCA集合
  • 要求计算 $\sum_r |S_r|$

本题是思维题,与LCA的求法无关,先看官方的AC码吧

AC码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

void solve() {

int n, k;
cin >> n >> k;
vector<vector<int>> a(n + 1);
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
a[u].push_back(v);
a[v].push_back(u);
}

//首先是建树,没什么好说的。

vector<int> sz(n + 1, 1);
auto dfs = [&] (auto &dfs, int v, int p) -> void{
for (int u : a[v]) {
if (u != p) {
dfs(dfs, u, v);
sz[v] += sz[u];
}
}
};

dfs(dfs, 1, 0);

//这里dfs是在求以i为节点的子数结点个数(包括i自己),结果存在sz[i]中。

//你要问了:树的根不是在变吗,树的结构也在变啊!为什么可以确定子树呐?
//后面就知道了,是为了方便分块。

ll ans = 0;
for (int i = 1; i <= n; i++) {
if (n - sz[i] >= k) ans += sz[i];
if (sz[i] >= k) ans += n - sz[i];
}
cout << ans + n << endl;

//最重要的一步,单拎出来讲。

}

int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}

核心代码

1
2
3
4
5
6
ll ans = 0; 
for (int i = 1; i <= n; i++) {
if (n - sz[i] >= k) ans += sz[i];
if (sz[i] >= k) ans += n - sz[i];
}
cout << ans + n << endl;

我们定义 (r, v): 以r为根时,存在一种k-集合,使得v是这k个节点的LCA
那么,这里的ans加的就是(r, v)的个数,只不过:
这里的 i 并不代表 r

我们来看第三个样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
默认以 1 为 DFS 根:

1
/ \
2 3
/ \ \
4 5 6

sz[1] = 6,
sz[2] = 3,
sz[3] = 2,
sz[4] = 1,
sz[5] = 1,
sz[6] = 1;
r S_r
1 {1, 2}
2 {1, 2}
3 {1, 2, 3}
4 {1, 2, 4}
5 {1, 2, 5}
6 {1, 2, 3, 6}

所以所有 (r, v) 对一共 17 个:

1
2
3
4
5
6
r=1 : (1,1), (1,2)
r=2 : (2,1), (2,2)
r=3 : (3,1), (3,2), (3,3)
r=4 : (4,1), (4,2), (4,4)
r=5 : (5,1), (5,2), (5,5)
r=6 : (6,1), (6,2), (6,3), (6,6)

样例模完,我们来看代码如何实现:

1
2
3
4
5
6
ans = 0
for (int i = 1; i <= n; i++) {
if (n - sz[i] >= k) ans += sz[i];
if (sz[i] >= k) ans += n - sz[i];
}
最后:ans += n;

i = 1

n - sz[1] = 0 < 3

sz[1] = 6 >= 3 ✅ ans += 0

这一轮循环没加任何东西,但别急,1 的 (r=1, v=1) 在最后 +n 里。(r=1, v =2)呢?在后面。

i = 2

1
2
3
4
5
    1
/ \
2 3
/ \ \
4 5 6

n - sz[2] = 3 >= k

这里指的是:因为结点2的子树外的元素个数 >= k,所以 以结点2的子树中的任意元素为根时,结点1(2的父结点)绝对可以作为LCA!(选这k个就行)

  • r ∈ 子树(2) = {2,4,5}
  • 贡献的是:(2,1), (4,1), (5,1), ans += sz[2]

sz[2] = 3 >= k

这里指的是:因为结点2的子树的元素个数 >= k,所以 以结点2的子树外的任意元素为根时,结点2绝对可以作为LCA!(选这k个就行)

  • r ∈ 子树外 = {1,3,6}
  • 贡献的是:(1,2), (3,2), (6,2), ans += n - sz[2]

最后一步:ans += n

ans += 6

👉 对应所有 (r = v):

(1,1), (2,2), (3,3), (4,4), (5,5), (6,6)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
最后把所有被加过的对合并

(1,1)
(1,2)

(2,1)
(2,2)

(3,1)
(3,2)
(3,3)

(4,1)
(4,2)
(4,4)

(5,1)
(5,2)
(5,5)

(6,1)
(6,2)
(6,3)
(6,6)

正好 17 个,一个不少,一个不多

为什么不会重复?

首先你在怕什么?

怕(r,v)被重复记入ans!

我们证明:任意合法的 (r, v) 不会被重复计入 ans。

假设相反,存在一对 (r, v),它在两轮不同的循环 i = x 和 i = y (x ≠ y) 中都被统计。

假设k = 3,以(2, 1)成立(当以2为根时,1可以作为LCA)为例,由AC码得:

考虑到子树2(i = 2),有(2, 1)会是下例情况:

1
2
3
4
5
6
7
    1
/ \
2 3
\
4

此时选{1,3,4}可得LCA=1.

如果考虑到子树3(i = 3),仍有(2, 1),因为 (r, v)中,v只会是i自己i的父结点,所以1是3的父结点,此时要满足(2, 1)成立,则2必须是3的子结点,与上图1是2的父结点矛盾!

1
2
3
4
5
6
7
    1
/ \
2 - 3
\
4

此时不是树结构!

得证!

Problem - C - Codeforces

这题有2个解法,然而我赛时一个没写出来,这不是废物是什么?

解法一

对于任意区间 [L,R]

换算一下,得到:

注意到

求$\Delta$的最大值即可

怎么没写出来呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <bits/stdc++.h>
using namespace std;

const int N = 1e9 + 5;

void solve(){

int n;
cin >> n;
vector<int> a(n + 1), pre(n + 1);
for (int i = 1; i <= n; ++i) {
cin >> a[i];
pre[i] = pre[i - 1] + a[i];
}

int mx1 = 0, mx2 = 0;
for (int i = 1; i <= n; ++i) {
int x = pre[i - 1] - i * (i - 1);
mx1 = max(x, mx1);
x = i * (i + 1) - pre[i];
mx2 = max(x, mx2);
}

cout << pre[n] + mx1 + mx2 << endl;
}

int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int _ = 1;
cin >> _;
while (_--)
solve();
return 0;
}

这是错的,因为L要小于R,而我取了全局最大:

1
2
3
4
5
6
7
int mx1 = 0, mx2 = 0;
for (int i = 1; i <= n; ++i) {
int x = pre[i - 1] - i * (i - 1);
mx1 = max(x, mx1);
x = i * (i + 1) - pre[i];
mx2 = max(x, mx2);
}

正解是:

1
2
3
4
5
6
7
int mx = 0, ans = 0;
for (int i = 1; i <= n; ++i) {
int x = pre[i - 1] - i * (i - 1);
mx = max(x, mx);
x = i * (i + 1) - pre[i] + mx;
ans = max(x, ans);
}

这样可以保证L<=R

妙啊

AC码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <bits/stdc++.h>
using namespace std;

const int N = 1e9 + 5;

void solve(){

int n;
cin >> n;
vector<int> a(n + 1), pre(n + 1);
for (int i = 1; i <= n; ++i) {
cin >> a[i];
pre[i] = pre[i - 1] + a[i];
}

int mx = 0, ans = 0;
for (int i = 1; i <= n; ++i) {
int x = pre[i - 1] - i * (i - 1);
mx = max(x, mx);
x = i * (i + 1) - pre[i] + mx;
ans = max(x, ans);
}

cout << pre[n] + ans << endl;
}

int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int _ = 1;
cin >> _;
while (_--)
solve();
return 0;
}

解法二

对于任意区间 [L,R]

原区间和为:

替换后的区间和:

神奇的来了,注意到

这是什么?

这是等差序列的求和公式!:

$$S_n = \dfrac{{项数}\times (\text{首项} + \text{末项})}{2}$$

所以:

所以$T$也可以写成:

$$\displaystyle{2 \times \sum_{i=L}^{R}} i = \displaystyle{ \sum_{i=L}^{R}} 2i$$

所以$\Delta$可以写成:

$$\displaystyle{\Delta = T - \displaystyle\sum_{i=L}^{R} a_i = \sum_{i=L}^{R} {(2i - a_i)}}$$

于是问题简化成:

寻找区间,使得 $\displaystyle{\Delta = \sum_{i=L}^{R} {(2i - a_i)}}$ 最大

这已经是标准最大子段和问题了

AC码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include<bits/stdc++.h>
using namespace std;

const int N=1e9+5;

void solve(){

int n; cin >> n;
vector<int> a(n + 1), b(n + 1);
long long sum = 0;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
sum += a[i];
b[i] = 2 * i - a[i];
}

long long ans = 0, tot = 0;
for (int i = 1; i <= n; ++i) {
tot = max(0LL, tot + b[i]);
ans = max(tot, ans);
}

cout << ans + sum << endl;

}

int main(){

ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
cin>>_;
while(_--)
solve();
return 0;

}

简单二进制状压

Problem - C - Codeforces

打表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include<bits/stdc++.h>
using ll = long long;
using namespace std;

const int N=1e9+5;

void solve(){

ll n; cin >> n;
vector<int> a(n + 1);
int ans = 0;
for (int i = 1; i <= n; ++i) {
a[i] = i;
ans += i * (n - i + 1);
}
int cnt = 0;
do {
int sum = 0;
for (int i = 1; i <= n; ++i) {
int mn = 0x3f3f3f3f;
for (int j = i; j <= n; ++j) {
mn = min(mn, a[j]);
sum += mn;
}
}

if (sum == ans){
++cnt;
cout << "\033[0m" << cnt << ": ";
for (int i = 1; i <= n; ++i) {
if (a[i] == 3) cout << "\033[32m" << a[i] << " ";
else cout << "\033[31m" << a[i] << " ";
}
cout << endl;
}
}while(next_permutation(a.begin() + 1, a.end()));

}

int main(){

//ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
while(_--)
solve();
system("pause");
return 0;

}

输入5为例:

1763829785661

1763829837661

不难发现:一半1,一半1里有一半2,一半2里又有一半3…

超过半数往后站,不过半往前站

因此有解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include<bits/stdc++.h>
using ll = long long;
using namespace std;

void solve(){

ll n, k; cin >> n >> k;
ll m = n;
vector<int> a(n + 1);

ll sum = 1LL;
for (int i = 1; i < n; ++i) {
sum <<= 1LL;
if (sum >= 1e12) {
sum = 1e12;
break;
}
}

if (k > sum) {
cout << -1 << endl;
return;
}

int l = 1, r = n;
ll ls = 0LL, rs = sum;
for (int i = 1; i <= n; ++i) {
ll mid = ((ls + rs) >> 1LL);
if (k <= mid) {
a[l] = i; //不过半往前站
++l;
rs = mid; //集中前半段
}
else {
a[r] = i; //超过半数往后站
--r;
ls = mid; //集中后半段
}
}

for (int i = 1; i <= n; ++i) {
cout << a[i] << " ";
}
cout << endl;

}

int main(){

ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
cin>>_;
while(_--)
solve();
return 0;

}

但是,这么解是错的,让我们听听AI怎么说。

对于

1
2
3
4
5
6
7
输入:
1
60 2

我输出的是: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 40 60 59 58 57 56 55 54 53 52 51 50 49 48 47 46 45 44 43 42 41 39

正确答案是: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 60 59

为什么错了?

你把 sum = 2^(n-1) 人为截断成 1e12
n=60 时真实的 $2^{n-1}=2^{59}≈5.76e17$,而你把它设成 1e12。以后你用 [ls,rs) 以二分区间把 k 映射成“左右选择”的逻辑 依赖 rs-ls 恰好是 2^(remaining) 的幂次。截断后这个不再成立,映射就被彻底破坏了 —— 因此决策在第 ~39 次才从“放左”变成“放右”,产生你看到的 1..38 然后一大段乱序的结果。

什么意思,原本sum很长很长,可以截很多个二分,但因为原本sum太长太长了,long long存不下,我把它截成 1e12(k的最大值),导致截的二分数变少了,mid很快扰乱了如下代码的判断

1
2
3
4
5
6
7
8
9
10
11
12
13
for (int i = 1; i <= n; ++i) {
ll mid = ((ls + rs) >> 1LL);
if (k <= mid) {
a[l] = i; //不过半往前站
++l;
rs = mid; //集中前半段
}
else {
a[r] = i; //超过半数往后站
--r;
ls = mid; //集中后半段
}
}

不妨想一下:

二进制状压

我们这样打表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include<bits/stdc++.h>
using ll = long long;
using namespace std;

const int N=1e9+5;

void solve(){

ll n; cin >> n;
vector<int> a(n + 1);
int ans = 0;
for (int i = 1; i <= n; ++i) {
a[i] = i;
ans += i * (n - i + 1);
}
int cnt = 0;
do {
int sum = 0;
for (int i = 1; i <= n; ++i) {
int mn = 0x3f3f3f3f;
for (int j = i; j <= n; ++j) {
mn = min(mn, a[j]);
sum += mn;
}
}

if (sum == ans){
++cnt;
cout << "\033[0m" << bitset<4>(cnt - 1)<< ": "; //以5为例(0 ~ 15) 16种情况
for (int i = 1; i <= n; ++i) {
if (a[i] == 1) cout << "\033[32m" << a[i] << " ";
else cout << "\033[31m" << a[i] << " ";
}
cout << endl;
}
}while(next_permutation(a.begin() + 1, a.end()));

}

int main(){

//ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
while(_--)
solve();
system("pause");
return 0;

}

img

我们可以想到,我们之前的步骤中,每一次二分相当于消去最高位,观察下一位,如果该位是0,往前站,该位是1,往后站,最后剩下的一位,填最后一位(n)。

假设n = 5, 当前位置为k = 8,由于从0计数,所以减1,得7,二进制为0111,于是,我们有:

1
2
3
4
5
6
0111: 1
111: 1 2
11: 1 3 2
1: 1 4 3 2

最后:1 5 4 3 2

完美!

我们可以反过来,从n到1填,毕竟从低位到高位遍历数位要容易很多,不断除2就行。填数就用双端队列一个个往里挤,很方便。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include<bits/stdc++.h>
using ll = long long;
using namespace std;

void solve(){

ll n, k; cin >> n >> k;
ll m = n;
vector<int> a(n + 1);

ll sum = 1LL;
for (int i = 1; i < n; ++i) {
sum <<= 1LL;
if (sum >= 1e12) {
sum = 1e12;
break;
}
}

if (k > sum) {
cout << -1 << endl;
return;
}

ll b = k - 1; //从0计数,减1
deque<ll> dq;
dq.push_back(n);
for (ll x = n - 1; x >= 1; --x) {
if (b & 1LL) dq.push_back(x);
else dq.push_front(x);
b >>= 1;
}

while(!dq.empty()) {
cout << dq.front() << " ";
dq.pop_front();
}
cout << endl;

}

int main(){

ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
cin>>_;
while(_--)
solve();
return 0;

}

AC了!

那你可能会想:前面说那么多,不还是观察出来的吗?

是的。

二维dp的回溯以及四维dp

P1005 NOIP 2007 提高组] 矩阵取数游戏 - 洛谷

这题我一开始用两次二维dp解,每次取最优路(类似贪心)。可是我不会dp回溯,于是有了以下题解:

二维dp的回溯

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
void back(int x, int y) {

if (x == 1 && y == 1) {
return;
}

if (x > 1 && dp[x - 1][y] + a[x][y] == dp[x][y]) {
back(x - 1, y);
}
else if (y > 1 && dp[x][y - 1] + a[x][y] == dp[x][y]) {
back(x, y - 1);
}
}

很好懂吧,因为每次dp取得都是到当前格的最大值,因此路径一定存在,一步步减回去就行。

完整码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include<bits/stdc++.h>
using ll = long long;
using namespace std;

const int N=1e9+5;

int dp1[11][11], dp2[11][11];
int a[11][11];

void back(int x, int y) {

if (x == 1 && y == 1) {
a[x][y] = 0;
return;
}

if (x > 1 && dp1[x - 1][y] + a[x][y] == dp1[x][y]) {
a[x][y] = 0;
back(x - 1, y);
}
else if (y > 1 && dp1[x][y - 1] + a[x][y] == dp1[x][y]) {
a[x][y] = 0;
back(x, y - 1);
}
}

void solve(){

int n; cin >> n;
while(1) {
int x, y, z;
cin >> x >> y >> z;
if (x == 0 && y == 0 && z == 0) {
break;
}
else {
a[x][y] = z;
}
}

int sum = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
dp1[i][j] = max(dp1[i - 1][j], dp1[i][j - 1]) + a[i][j];
}
}

sum += dp1[n][n];
back(n, n);
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
dp2[i][j] = max(dp2[i - 1][j], dp2[i][j - 1]) + a[i][j];
}
}

sum += dp2[n][n];
cout << sum << endl;

}

int main(){

ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
while(_--)
solve();
return 0;

}

注意两点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void back(int x, int y) {

// a[x][y] = 0; 不要写在这里,a[x][y]后面会用到,这里不要先改,到if后面再改 !!!

if (x == 1 && y == 1) {
a[x][y] = 0;
return;
}

if (x > 1 && dp1[x - 1][y] + a[x][y] == dp1[x][y]) { // x > 1 不要越界
a[x][y] = 0;
back(x - 1, y);
}
else if (y > 1 && dp1[x][y - 1] + a[x][y] == dp1[x][y]) { // 同理, y > 1 不要越界
a[x][y] = 0;
back(x, y - 1);
}
}

果然pa了,因为当前最优不代表全局最优,贪心思路不可行,反例:

7
1 2 2
1 3 3
2 2 3
3 2 3
5 4 4
6 4 4
7 2 2
7 4 4
0 0 0

1
2
3
4
5
6
7
0 2 3 0 0 0 0
0 3 0 0 0 0 0
0 3 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 4 0 0 0
0 0 0 4 0 0 0
0 2 0 4 0 0 0

答案是25,然而我的解法第一次是:

1
2
3
4
5
6
7
0 0 3 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 2 0 0 0 0 0

得20,第二次得23。

实际上是第一次:

1
2
3
4
5
6
7
0 0 3 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 4 0 0 0
0 0 0 4 0 0 0
0 0 0 0 0 0 0

第二次全吞完,得25。

所以得用四维dp做。

四维dp

直接上代码吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include<bits/stdc++.h>
using ll = long long;
using namespace std;

int dp[11][11][11][11]; // dp[i][j][p][q]表示第一遍走到点(i, j),第二遍走到点(p, q)的最优解
int a[11][11];

void solve(){

int n; cin >> n;
while(1) {
int x, y, z;
cin >> x >> y >> z;
if (x == 0 && y == 0 && z == 0) {
break;
}
else {
a[x][y] = z;
}
}

for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
for (int p = 1; p <= n; ++p) {
for (int q = 1; q <= n; ++q) {
dp[i][j][p][q] = max(max(dp[i-1][j][p-1][q], dp[i-1][j][p][q-1]), max(dp[i][j-1][p-1][q], dp[i][j-1][p][q-1])) + a[i][j] + a[p][q];
if (i == p && j == q) dp[i][j][p][q] -= a[i][j];
}
}
}
}

cout << dp[n][n][n][n] << endl;

}

int main(){

ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
while(_--)
solve();
return 0;

}

从逐次减法到高效除法

题目描述:

给定初始值 N,以及 M 种操作,每种操作 (a, b),表示每次操作必须满足 N >= a,然后 N 会减少 b。要求计算最多可以执行多少次操作,并更新 N


思路与推导:

  1. 操作描述
    每个操作 (a, b),表示:

    • 只有当 N >= a 时,才能进行操作。
    • 每次操作会将 N 减去 b,直到无法继续执行为止。
  2. 最大操作次数的推导
    假设对于某个操作 ab,我们希望计算最多可以执行多少次该操作。

    由于每次操作将 N 减少 b,为了保证 N 始终大于等于 a,我们可以得到以下条件:

    其中 t 为最大执行次数。由此可以得到:

    这意味着最多可以进行 $t = \left\lfloor \frac{N - a}{b} \right\rfloor$ 次操作。

    为什么要加 1

    • 因为N在减了t次之后还是大于或等于a的还可以再进行一次操作,这次减了b了之后,N小于a,不会再进行下一次(a,b)操作了

    所以,最终的最大操作次数是:

  3. 更新 N
    每次操作执行 t 次后,N 会减少 t * b。更新后的 N 为:

    继续进行下一个操作,直到遍历所有操作。


示例:

输入:

1
2
3
100 2
20 3
50 5

输出:

1
27

解释:

  1. 第一个操作a = 20, b = 3,计算 (100 - 20) / 3 + 1 = 27,执行 27 次,更新 N = 100 - 27 * 3 = 19
  2. 第二个操作a = 50, b = 5,由于 N = 19 小于 50,跳过该操作。
  3. 最终输出 cnt = 27

总结:

通过优化循环,使用数学公式一次性计算每个操作的最大执行次数,避免了逐次减法,从而提高了程序效率。核心考察了通过数学推导解决动态问题和优化计算时间的能力。

https://ac.nowcoder.com/acm/contest/120454/F

属于典型的“是否存在一个 X ∈ R 可以满足所有约束”的问题

浮点二分

二分解法: 二分答案$r^2$,然后将区间$x-\sqrt{r^2-y^2}$到$x+\sqrt{r^2-y^2}$ 区间覆盖判断,若所有区间存在交集,则返回true

先看错解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include<bits/stdc++.h>
using namespace std;
struct node {
double x, y;
};

void solve(){

int n; cin >> n;
vector<node> a(n + 1);
for (int i = 1; i <= n; ++i) {
cin >> a[i].x >> a[i].y;
}
auto check = [&](double R2) -> bool {
double L = -1e9, R = 1e9;
for (int i = 1; i <= n; ++i) {
double X = R2 - a[i].y * a[i].y;
double l = a[i].x - sqrt(X);
double r = a[i].x + sqrt(X);
L = max(L, l);
R = min(R, r);
}
return L <= R;
};
double l = -1e9, r = 1e9;
double ans;
while (l <= r) {
double mid = l + (r - l) / 2.0;
if (check (mid)) {
ans = mid;
l = mid + 1;
}
else {
r = mid - 1;
}
}
cout << sqrt(ans) << endl;

}

int main(){

ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
while(_--)
solve();
return 0;

}

错误点:

  • 在二分一个 double,却使用了整数二分写法

    1
    2
    3
    4
    5
    while (l <= r) {
    mid = l + (r - l) / 2;
    if (check(mid)) l = mid + 1;
    else r = mid - 1;
    }

    使用这种比较完全不收敛,
    而且 mid + 1 会跳过正确答案,导致永远不精确。

  • check(R2) 没有边界判断

    如果 $r^2$ < $y^2$,$x^2$为负,会 sqrt(负数) → nan → WA/RE

  • 二分区间不应该是 [-1e9, 1e9]

    [-1e9, 1e9] 有一半是无效区间
    因为我们二分答案 $r^2$ ,而半径平方不能为负

正确写法:浮点二分(模板)

1
2
3
4
5
6
7
double l = 0, r = 2e8;   // 1e4^2 + 1e4^2
for (int i = 0; i < 60; i++) {
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}

不能用 while(l <= r)!!

最终无 bug 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include<bits/stdc++.h>
using namespace std;
struct node {
double x, y;
};

void solve(){

int n; cin >> n;
vector<node> a(n + 1);
for (int i = 1; i <= n; ++i) {
cin >> a[i].x >> a[i].y;
}
auto check = [&](double R2) -> bool {
double L = -1e9, R = 1e9;
for (int i = 1; i <= n; ++i) {
double X = R2 - a[i].y * a[i].y;

if (X < 0) return 0; //若圆与x轴无交点,即X < 0,直接返回false,否则sqrt(X)会nan

double l = a[i].x - sqrt(X);
double r = a[i].x + sqrt(X);
L = max(L, l);
R = min(R, r);
}
return L <= R;
};

double l = 0, r = 1e9;
for (int i = 1; i <= 60; ++i) {
double mid = l + (r - l) / 2;
if (check(mid)) {
r = mid; //这里应该以r为答案,之前连这里都错了!
}
else {
l = mid;
}
}

cout << setprecision(6) << sqrt(r) << endl;

}

int main(){

ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
while(_--)
solve();
return 0;

}

三分

三分法是一种用于在“单峰/单谷函数f(x)”上寻找最优值(最大或最小)的算法。

三分法思想:

把区间 [l, r] 分成三段: l —— m1 —— m2 —— r

其中: m1 = l + (r - l) / 3 (左三等分点)
m2 = r - (r - l) / 3(右三等分点)

然后比较: f(m1) 和 f(m2)

如果 f(m1) < f(m2)
→ 最高点在区间 (m1, r)
→ 扔掉左边部分 l = m1

如果 f(m1) > f(m2)
→ 最高点在区间 (l, m2)
→ 扔掉右边部分 r = m2

三分法代码模板(整数区间)

1
2
3
4
5
6
7
8
9
10
11
12
int ternary(int l, int r) {
while (r - l >= 3) {
int m1 = l + (r - l) / 3;
int m2 = r - (r - l) / 3;

if (f(m1) < f(m2))
l = m1;
else
r = m2;
}
}

三分法代码模板(实数区间)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
double ternary(double l, double r) {
const double eps = 1e-12;

while (r - l > eps) {
double m1 = l + (r - l) / 3;
double m2 = r - (r - l) / 3;

if (f(m1) < f(m2)) {
l = m1;
} else {
r = m2;
}
}
}

算了,直接看题吧

我们要在 x 轴上选一个点 X,使得到所有点 ($x_i$, $y_i$) 的 最大距离最小

$f(x) = \max\limits_{1 \le i \le n}\sqrt{(X - x_i)^2 + y_i^2}$

自己感受一下:$X$从x轴左边到x轴右边,$\max\limits_{1 \le i \le n}\sqrt{(X - x_i)^2 + y_i^2}$ 肯定先变小后变大

所以这个函数 是凹的(单峰),我们要搜$f(x)$ 的最小值(对应$x$为X),因此可用 三分法 搜索 X。

代码(AC 版本,C++)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include<bits/stdc++.h>
using namespace std;

struct node {
int x, y;
};

void solve(){

int n; cin >> n;
vector<node> a(n + 1);
for (int i = 1; i <= n; ++i) {
cin >> a[i].x >> a[i].y;
}

auto f = [&](double x) -> double {
double mx = 0;
for (int i = 1; i <= n; ++i) {
double dx = a[i].x - x, dy = a[i].y;
mx = max(mx, sqrt(dx * dx + dy * dy));
}
return mx;
};

double l = -10000, r = 10000;
for (int i = 1; i <= 60; ++i) {
double m1 = l + (r - l) / 3;
double m2 = r - (r - l) / 3;
if (f(m1) > f(m2)) {
l = m1;
}
else {
r = m2;
}
}

cout << setprecision(6) << f((l + r) / 2.0) << endl;

}

int main(){

ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
while(_--)
solve();
return 0;

}

收工!

逆推概率笔记

一、背景

  • 题目目标:计算多个区间随机出现时,每个格子恰好被一个区间覆盖的总概率。
  • 简化版本:去掉取模、逆元,用浮点数 p/q 表示区间出现概率。

二、核心流程

  1. 初始化 “都不出现” 的概率乘积ans

    • 每条区间不出现概率 = (q - p)/q
    • ans = ∏ (q - p)/q
  2. 动态规划 (DP)

    • 状态:dp[i] 表示从格子 1 到 i,在“都不出现”的前提下,恰好被覆盖一次的概率。
    • 初始:dp[0] = 1dp[1..m] = 0
  3. 区间排序与转移

    • 先按 lr 排序区间。
    • 对第 i 条区间 [l, r] 做:

      1
      dp[r] += dp[l - 1] * (p / (q - p));
  4. 结果计算

    • 最终答案 = ans * dp[m]

三、关键疑问与解答

问题1:为什么要除以 (q - p)

  • 直觉误区:在“不出现”情况下,出现概率似乎为 0。
  • 条件概率:反向推导就是在“不出现”的条件下,推导“出现”的修正因子。
  • 公式推导
    $P(出现|不出现) = \frac{P(出现)}{P(不出现)} = \frac{(p/q)}{((q-p)/q)} = \frac{p}{q - p}.$

问题2:什么是“反向推导”?

  • 指在“都不出现”的概率空间中,逆向演算某条区间突然出现对最终结果的影响。
  • 作用:给 dp 转移添加正确的概率修正因子。

四、小例子演示

  • m = 3, 区间 A:[1,2], p/q=1/2; B:[3,3], p/q=1/3
  • ans = (1/2)*(2/3) = 1/3
  • DP 转移:

    1. A: dp[2] += dp[0] * (1/(2-1)) = 1dp[2] = 1
    2. B: dp[3] += dp[2] * (1/(3-1)) = 1/2dp[3] = 0.5
  • 最终:ans * dp[3] = 1/3 * 1/2 = 1/6

五、简化版代码(去模版、取模、快速幂)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

struct Node {
double l, r, p, q;
};

bool cmp(Node a, Node b) {
if (a.l == b.l) return a.r < b.r;
return a.l < b.l;
}

int main() {
int n, m;
cin >> n >> m;
vector<Node> seg(n);
double ans = 1.0;

for (int i = 0; i < n; ++i) {
cin >> seg[i].l >> seg[i].r >> seg[i].p >> seg[i].q;
ans *= (seg[i].q - seg[i].p) / seg[i].q;
}

sort(seg.begin(), seg.end(), cmp);
vector<double> dp(m + 5, 0);
dp[0] = 1.0;

for (int i = 0; i < n; ++i) {
dp[(int)seg[i].r] += dp[(int)seg[i].l - 1] * seg[i].p / (seg[i].q - seg[i].p);
}

cout << ans * dp[m] << endl;
return 0;
}

备注:以上笔记可帮助队友快速理解“为什么除以 (q-p)”及反向推导思路。

六、示意图解

1
2
3
4
5
6
7
8
9
10
11
12
起点 dp[0] = 1.0
|
+-- 区间 A [1,2]
p/q = 1/2,不出现概率 (q-p)/q=1/2
修正因子 p/(q-p)=1/(2-1)=1
=> dp[2] += dp[0] * 1 = 1.0
|
+-- 区间 B [3,3]
p/q = 1/3,不出现概率 (q-p)/q=2/3
修正因子 p/(q-p)=1/(3-1)=1/2
=> dp[3] += dp[2] * 1/2 = 0.5
最终答案 = ans * dp[3] = (1/2*2/3) * 0.5 = 1/6

欧拉函数 (Euler’s Totient Function) 详解

欧拉函数 φ(n) 是数论中一个非常重要的函数,它表示小于或等于 n 的正整数中与 n 互质的数的个数。

定义

对于正整数 n,欧拉函数 φ(n) 定义为:

1
φ(n) = 小于或等于 n 的正整数中与 n 互质的数的个数

其中”互质”意味着两个数的最大公约数为1。

计算方法

1. 对于质数 p

如果 p 是质数,那么:

1
φ(p) = p - 1

因为1,2,…,p-1都与p互质。

2. 对于质数的幂 pᵏ

1
φ(pᵏ) = pᵏ - pᵏ⁻¹ = pᵏ(1 - 1/p)

因为在1到pᵏ之间,只有p的倍数不与pᵏ互质,这样的数有pᵏ⁻¹个。

3. 对于任意正整数 n

利用中国剩余定理和积性函数性质,如果n的标准质因数分解为:

1
n = p₁ᵏ¹ × p₂ᵏ² × ... × pₘᵏᵐ

那么:
1
φ(n) = n × (1 - 1/p₁) × (1 - 1/p₂) × ... × (1 - 1/pₘ)

示例计算

  1. φ(10):

    1
    2
    10 = 2 × 5
    φ(10) = 10 × (1 - 1/2) × (1 - 1/5) = 10 × 1/2 × 4/5 = 4

    实际与10互质的数有1,3,7,9,共4个。

  2. φ(12):

    1
    2
    12 = 2² × 3
    φ(12) = 12 × (1 - 1/2) × (1 - 1/3) = 12 × 1/2 × 2/3 = 4

    实际与12互质的数有1,5,7,11,共4个。

在代码中的应用

在你提供的代码中,计算了φ(210):

1
2
3
4
210 = 2 × 3 × 5 × 7
φ(210) = 210 × (1 - 1/2) × (1 - 1/3) × (1 - 1/5) × (1 - 1/7)
= 210 × 1/2 × 2/3 × 4/5 × 6/7
= 48

这意味着在每210个连续整数中,有48个数不被2,3,5,7整除(即与210互质)。

性质

  1. 积性函数:如果m和n互质,则φ(mn) = φ(m)φ(n)
  2. 欧拉定理:如果a和n互质,则a^φ(n) ≡ 1 (mod n)
  3. 费马小定理:当n是质数p时的特例,a^(p-1) ≡ 1 (mod p)

欧拉函数在密码学(如RSA算法)、数论和组合数学中都有广泛应用。

位运算小技巧总结

1. 奇偶性快速判断与反转

1
2
3
4
5
// 判断奇偶性
bool is_odd = x & 1; // 真为奇数,假为偶数

// 0/1交替
n ^= 1;

2. 二进制位操作

1
2
3
4
5
6
7
8
// (1) 取第k位(从0开始)
int bit = (n >> k) & 1;

// (2) 设置第k位为1
n |= (1 << k);

// (3) 设置第k位为0
n &= ~(1 << k);

3. 位遍历(判断当前位是 1 还是 0 )

1
2
3
4
5
6
7
8
9
while(num) {           // 当num不为0时循环
if(num & 1) {
// 当前位为1的处理逻辑
}
else{
// 当前位为0的处理逻辑
}
num >>= 1; // 右移一位(相当于除以2)
}

4. 快速计算二进制1的个数(__builtin_popcount)

1
2
3
4
5
6
7
int cnt = __builtin_popcount(x); // GCC内置函数
// 等价于:
int cnt = 0;
while(x) {
x &= x - 1; // 清除最低位的1
++cnt;
}

5. 保留最低位的1(lowbit)

1
2
int lowbit = x & -x;  // 树状数组核心操作
// 例如:x=6(110) → 2(10)

6. 判断/寻找0位

1
2
3
4
5
6
7
8
int t = 1ll;
for (int i = 0; i <= 63;++i){
if(!(n&t)){ //核心判断条件
//n的第i位为0
}

t <<= 1;
}

7. 判断是否为2的幂

1
2
bool is_power_of_two = (x & (x - 1)) == 0;
// 注意特判x=0时为false

8. 位运算优先级口诀

1
~ > 算术运算 > << >> > & > ^ > |

注意事项

  1. 位运算优先级容易出错,建议多用括号
  2. 右移时注意符号位(无符号用>>,有符号用>>>
  3. 大数运算记得加1LL防止溢出