模逆元/乘法逆元

众所周知,如同线段树/FFT一样,乘法逆元是CF的入门必会算法,然而我只能凭空写线段树

每次遇到需要用乘法逆元,我都是快速搜一个函数代码,复制进来用

之前也反复学懂了三四遍,始终没记住(毕竟用的频率不算高)

以下大自整理总结一下

什么是乘法逆元

已知a和p求b, 其中gcd(a,p)=1

$a^{ -1 } \equiv b{\pmod {n}}$

求法

费马小定理

当p是质数时

$a^{p-2} \mod p $

扩展欧几里得

既解$\gcd=1$的扩展欧几里得方程, 已知$(a,p)$求一个$(b,k)$

$a\cdot b+p\cdot k = 1$, 其中$k$为某未知整数

把正常计算$\gcd$的过程的细节 写出来int gcd(int a,int b){return b==0?a:gcd(b,a%b);}

$a \cdot b+p\cdot k = 1$,

初始为$(a,p)$

把$(d_i,d_{i+1})$ 变为 $(d_{i+1},d_{i+2})$

$d_1 = a + p\cdot c_1$ 分解 式子1 ($c_1 = - a / p, d_1 = a \mod p$)

$d_2 = p + d_1\cdot c_2$ 分解 式子2($c_2 = - p / d_1, d_2 = p \mod d_1$)

$d_3 = d_1 + d_2\cdot c_3$ 分解 式子3

直到$d_i == 0$, 也就是$d_{i+1} = d_{i-1} + d_2 \cdot c_{i+1}$

$gcd(a,p) = gcd(a,p) \cdot 1 + 0 cdot 0$

在算法实现上也就是再分解, 然后反过来把式子i带入式子i+1

$d_n = d_{n-2} + d_{n-1} \cdot c_n$

$d_n = d_{n-2} + (d_{n-3}+d_{n-2} \cdot c_{n-1}) \cdot c_n$

然后我们只需要记录的是 $d_{n-2}, d_{n-1}$的系数 $gcd(a,p) = d_{n-2} \cdot k_0 + d_{n-1} \cdot k_1$

$gcd(a,p) = d_{n-2} \cdot k_0 + (d_{n-3} \cdot 1 + d_{n-2} \cdot c_{n-1}) \cdot k_1$ , 这个 $c_i = - a_i/b_i$ 在回溯过程中 可以得到

$gcd(a,p) = d_{n-3} \cdot k_1 + d_{n-2} \cdot (k_0 - k_1 \cdot a_i / b_i )$

如此展开到最初的a和p即可

1
2
3
4
5
6
7
// 返回的 [x,y,z] 满足 a*x+b*y=z
tuple<int,int,int> exgcd(int a,int b){
if(b==0)
return {1,0,a};
auto [x,y,z] = exgcd(b,a%b);
return {y,x-(a/b)*y,z};
}

以上代码需要C++17

要注意的是如果 原来的数据有负数,零,需要在外部处理

扩展欧几里得 的矩阵理解

${\begin{pmatrix} a & p \\ 1 & 0 \\ 0 & 1 \end{pmatrix} } {\begin{pmatrix}x \\ y\end{pmatrix}} \Rightarrow {\begin{pmatrix}{a \cdot x + b \cdot y = gcd(x,y) } \\ x \\ y \end{pmatrix}}$

所以可以建立 2*3矩阵,然后对它做列变换,直到 得到gcd(a,p), 这时的 列向量上的 第二第三个值就是(x,y)

批量求 $< p $ 的 模逆元

方法1

妇孺皆知,质数的欧拉筛法,又 模逆元 有 可乘性,

所以 如果 a=p1*p2,那么 a的模逆元 = p1的模逆元*p2的模逆元 mod p

只需要 O(p+ exgcd平均时间 * ~n/ln n )

方法2

线性递推

$x = p/a$ (整除)

$y = p \mod a < a$

有 $a \cdot x + y = p$

所以 $y = - a \cdot x (\bmod p)$

两边同时乘 逆元 $inv_a \cdot inv_y$

$inv_a \cdot inv_y \cdot y = - a \cdot inv_a \cdot inv_y \cdot x (\bmod p)$

即 $inv_a = - x \cdot inv_y = - (p/a) \cdot inv_{p \bmod a} (\bmod p)$

注意到$ y = p \bmod a < a$,所以 在计算$inv_a$时,$inv_y$ 已经计算好了

又注意处理负号,在mod p的意义下

$- (p/a) \cdot inv_{p \bmod a} = (p - (p/a)) \cdot inv_{p \bmod a} (\bmod p)$

综上

1
2
3
inv[1] = 1;
rep(i,2,p)
inv[i] = ((p-p/i)*inv[p%i])%p;

例题

CF EDU57 F

参考

wikipedia

baidu百科

USACO 5.1.1

简述

给你n(0<=n<=10000)个平面上点,求凸包的周长

坐标范围(-1'000'000<=x,y<=1'000'000)

解法

官网介绍的方法

关于此题

实现和介绍方法的差异

首先USACO介绍的方法选取的一个中间的点,然后对周围的点排序,

关于这个,我这里实现是选取y最小(同理可以选取任何一个斜率上的边界点(因为它必定属于凸包(或位于凸包边上 共线(也是属于啦))))

这就会导致所用的排序不太相同[我这样也可以用角度直接比]

因为是某斜率边界点,那么 剩余的其它点必定在0至180度内,可以直接用叉乘比较,因此又需要注意,0和180度的比较上为了避免对称点的 不定偏序关系,需要选取 所选斜率上边界点上的边界点

有点拗口,具体举例,如果我选择y最小的点,假设有3个

A(-4,0),B(0,0),C(4,0),如果我选取B(0,0),作为比较的点的话,那么根据叉乘来排序,A(-4,0)C(4,0)之间的左右关系是可颠倒的BA×BC=0=BC×BA

所以这种情况,建议选择y最小时,x最小或x最大的.[不过幸运的是USACO数据 向来很弱,没有加处理仅仅取边界点也能AC]

而对于USACO介绍这种方法选中心点的时候,注意不应该用叉乘来作为比较函数,因为可能存在石头剪刀布的情况,(上面的0和180度也会发生)

举例有4个点A(-1,0),B(1,1),C(0,-1),O(0,0),当我们采取USACO介绍的方法进行排序时,假设选取O为中心的比较点,那么会有 剩下3个点 都在某另一个点左侧,也就没有偏序关系,无法排序,这种情况该用,计算角度进行排序


在选比较的点/中心点时,USACO还需要解决,绕了一圈以后,处理最初的点是否属于凸包的问题,而选取某一斜率边界点的话,不会有这个问题,因为该点一定在凸包边界上

实现

选取某斜率下的边界点(简单起见就y最小)

以该点为基础,排序(下面代码用逆时针排序),如果有多点共线,选取距离近的

for排序后的数组

  • 逐个尝试push加入点

  • 如果当前点已经有>1个点,且栈顶的两个点A=p[i-1],B=p[i],与新点Q,满足AB 不在 AQ右侧,(依赖 前面的排序和 距离从近到远),那么去掉栈顶也就是B,重新尝试push该Q

最后求周长就没啥说的了..


一点点正确性怀疑[都是被cf逼的],因为USACO要求的是 精度到小数点后2位,所以我有点想知道,这么多浮点数运算 损失了多少23333

代码

(没有处理上面说的0和180可能存在的问题,没有HACK的感觉真轻松,向CF日常200+测试数据低头)

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
76
77
78
/*
ID: 10157591
PROG: fc
LANG: C++
*/
#include <bits/stdc++.h>
#define rep(i,a,n) for (int i=a;i<n;i++)
#define per(i,a,n) for (int i=n-1;i>=a;i--)

using namespace std;

const string filename = "fc";

void usefile(){
freopen((filename+".in").c_str(),"r",stdin);
freopen((filename+".out").c_str(),"w",stdout);
}

int n;

pair<double,double> base = {0,0};
pair<double,double>p[10010];

// 右手定则, v1在v2右侧 为正, v1在v2左侧为负
double cross(const pair<double,double>& v1,const pair<double,double>& v2){
return v1.first * v2.second - v1.second * v2.first;
}

pair<double,double> operator - (const pair<double,double>& p1,const pair<double,double>& p2){
return {p1.first-p2.first,p1.second-p2.second};
}

double abs(const pair<double,double> & p){
return sqrt(p.first*p.first + p.second*p.second);
}

// 逆时针 ,共线从近到远
bool cmp(const pair<double,double>& p1,const pair<double,double>& p2){
double c = cross(p1-base,p2-base);
if(c != 0) return c>0;
return abs(p1-base) < abs(p2-base);
}

pair<double,double> ans[10010];

int main(){
usefile();
cin>>n;
if(n < 2){
cout<<0<<endl;
return 0;
}
rep(i,0,n){
scanf("%lf %lf",&p[i].first,&p[i].second);
}
base = p[0];
rep(i,0,n){
if(base.second > p[i].second){
base = p[i];
}
}
sort(p,p+n,cmp);
ans[0] = p[0];
int cnt = 1;
rep(i,1,n){
while(cnt>1 && cross(ans[cnt-1]-ans[cnt-2],p[i]-ans[cnt-1]) <= 0){ // 新加的点应该在最后一条射线左侧
cnt--;
}
ans[cnt++] = p[i];
}
double output = 0;
rep(i,0,cnt){
output+=abs(ans[(i+1)%cnt]-ans[(i)%cnt]);
}
printf("%.2lf\n",output);

return 0;
}

CF #526 Div1 C Max Mex

大意

给一个n个点的树,树上的点上带有值,这些值为0到n-1的排列

Q个询问:

询问1.(参数点的坐标) 交换两个点上的值

询问2.(无参数) 在树上找一条简单路径,使该简单路径上MEX最大,输出该最大值

(2<=n<=200'000)

(1<=q<=200'000)

MEX: 返回集合中最小未出现的自然数 如MEX({0,1,2,4,8,16}) = 3

解法

线段树维护[v1 -> vn] 这一段是否可能 在一条简单路径上,如果可能,那么记录这条简单路径的端点坐标

保存和更新,利用线段树更新的log级别,和LCA 来进行树上线段的合并


关于合并 举例解释:

如果我们的线段树要合并[l1->r1][l2->r2] , 其中l2=r1+1

通过递归处理先得到

[l1->r1]这一段值,对应在树上的简单路径的端点坐标为vl1vr1

[l2->r2]这一段值,对应在树上的简单路径的端点坐标为vl2vr2

注意到 前序遍历 和 后续遍历的特征,如果 点A是B的祖先,那么前序遍历A<B, 且后序遍历A>B

那么有如果这4个端点,在从根到某个叶子的一条链上(当且仅当),则这四个端点的 前序遍历的偏序关系 正好 和后续遍历相反.

所以 我们可以通过对这4个点排序, 前序最小值对应的点 是否和 后续最大值对应的点 相同.如果相同,那么这4个点在一个链上返回[找到的最深点,最潜点] O(1)

以上是 存在从根下来的链经过4个端点.

下面考虑是否存在一个跨某节点连接 该节点两个子链的简单路径,经过4个端点

(假设dfs的子节点枚举从左向右,便于描述)

注意到,如果有祖先关系,那么前序的最大值为深度最深的点,如果是非祖先关系,那么 前序的最大值为最右侧的点,也就有了 前序最大是最右侧最深的点(假设叫RV)

对称的,后序遍历最小为最左侧最深的一个点.(假设叫LV)

那么 实际要考察的就是四个端点是否都在从LV到RV的这样一条简单路径上,

那么也就是LCA+判断点是否在链上,走一波O(log 树的深度)

如果失败,则用记录不可合并[any way -1也好 额外字段也好]


线段树更新,就日常更新就好了

查询,0是必定可以进链的

那么 尝试把0 和 (0-n/2) 左半合并(因为链合并有幂等性 不用考虑0和0自己冲突)

如果可以合并,就把合并后的和右半合并

如果不行,则 合并目标 = 合并目标的左半; O(log n)

相关知识

线段树(建立&更新&查询),老生常谈了[CF 入门必会],[相对于后缀数组,相对更难写,但能维护更多的状态]

合并树上的链(LCA,dfs 前序 后序遍历 特点,求一个点是否在一个树中的一条链上(类似LCA))

LCA, 求树上点的最近公共祖先:(基于fa[点i][幂次k] = 点i的2^k祖先),实现O(log(树高的))的时间复杂度

代码

code

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
#define ten5 100000+10
#define MOD 1000000007
#define rep(i, a, n) for (int i=a;i<n;i++)
#define iif(c, t, f) ((c)?(t):(f))
#define per(i, a, n) for (int i=n-1;i>=a;i--)
#define pb push_back
#define mp make_pair
#define fi first
#define se second

const int N = 200010;
int n;
struct vertex {
int dep;
int v;
int _in;
int _out;
} i2v[N];
int v2i[N];

const int Log = 22;
int fa[N][Log + 1]; // fa[0][any] = 0 的性质很好
vector<int> child[N];

pair<int, int> T[N << 2];

void dfs(int i, int dep, int &idx) {
i2v[i].dep = dep;
i2v[i]._in = idx++;
for (auto item:child[i])
dfs(item, dep + 1, idx);
i2v[i]._out = idx++;
}

int Jump(int x, int d) {
if (d < 0)
return x;
per(i, 0, Log + 1){
if ((d >> i) & 1)
x = fa[x][i];
}
return x;
}

int LCA(int l, int r) {
int d = i2v[l].dep - i2v[r].dep;
if (d < 0) {
swap(l, r);
d = -d;
}
l = Jump(l, d);
if (l == r)
return l;
per(i, 0, Log + 1){
if (fa[l][i] != fa[r][i]) {
l = fa[l][i];
r = fa[r][i];
}
}
return fa[l][0];
}


bool onpath(int idx, int l_idx, int r_idx) {
int anc = LCA(l_idx, r_idx);
if (i2v[anc].dep > i2v[idx].dep)
return false;
return
Jump(l_idx, i2v[l_idx].dep - i2v[idx].dep) == idx ||
Jump(r_idx, i2v[r_idx].dep - i2v[idx].dep) == idx;
}

pair<int, int> mergeT(const pair<int, int> &v1, const pair<int, int> &v2) {
int arr[] = {v1.fi, v1.se, v2.fi, v2.se};
rep(i, 0, 4){
if (arr[i] == -1)
return {-1, -1};
}
int l_leaf = arr[0];
rep(i, 0, 4) {
if (i2v[arr[i]]._out < i2v[l_leaf]._out)
l_leaf = arr[i];
}

int r_leaf = arr[0];
rep(i, 0, 4) {
if (i2v[arr[i]]._in > i2v[r_leaf]._in)
r_leaf = arr[i];
}

if (l_leaf == r_leaf) { // 所有点在从根向下的一条链上
int rt = arr[0];
rep(i, 0, 4) {
if (i2v[arr[i]].dep < i2v[rt].dep)
rt = arr[i];
}
return {l_leaf, rt};
} else { // 链跨过了某节点连接某节点的两个子链
rep(i, 0, 4) {
if (!onpath(arr[i], l_leaf, r_leaf))
return {-1,-1};
}
}
return {l_leaf, r_leaf};
}

pair<int, int> initSeg(int o, int l, int r) {
if (l == r)
return T[o] = {v2i[l], v2i[r]};
int mid = (l + r) / 2;
return T[o] = mergeT(
initSeg(o << 1, l, mid),
initSeg(o << 1 | 1, mid + 1, r));
}

void updateSeg(int o, int l, int r, const int &val) {
if (l == r) {
T[o] = {v2i[val], v2i[val]};
return;
}
int mid = (l + r) / 2;
if (val <= mid)
updateSeg(o << 1, l, mid, val);
else
updateSeg(o << 1 | 1, mid + 1, r, val);
T[o] = mergeT(T[o << 1], T[o << 1 | 1]);
}

int query(int o, int l, int r, const pair<int, int> &now) {
if (l == r)
return mergeT(now, T[o]).fi == -1 ? l : l + 1;
int mid = (l + r) / 2;
auto tmp = mergeT(now, T[o << 1]);
if (tmp.fi == -1)
return query(o << 1, l, mid, now);
else
return query(o << 1 | 1, mid + 1, r, tmp);
}

int main() {
cin >> n;
rep(i, 1, n + 1) {
scanf("%d", &i2v[i].v);
v2i[i2v[i].v] = i;
}
rep(i, 2, n + 1) {
scanf("%d", &fa[i][0]);
child[fa[i][0]].pb(i);
}
rep(j, 1, Log + 1) {
rep(i, 1, n + 1) {
fa[i][j] = fa[fa[i][j - 1]][j - 1];
}
}
int idx = 1;
dfs(1, 1, idx);

initSeg(1, 0, n - 1);

int Q;
cin >> Q;
while (Q--) {
int t;
scanf("%d", &t);
if (t == 1) {
int i, j;
scanf("%d %d", &i, &j);
swap(v2i[i2v[i].v], v2i[i2v[j].v]);
swap(i2v[i].v, i2v[j].v);
updateSeg(1, 0, n - 1, i2v[i].v);
updateSeg(1, 0, n - 1, i2v[j].v);
} else {
pair<int, int> st = {v2i[0], v2i[0]};
printf("%d\n", query(1, 0, n - 1, st));
}
}
return 0;
}

USACO4.4.1

输入

给你n(2<=n<=32)个点,m(0<=m<=1'000)条边,可包含重边,每条边权重c(0<=c<=2'000'000)

求/输出

最大流

让最小割被割的边数量最小,求该最小值

求在该最小值下,求字典序最小的最小割

解法

忽略0边[如果不忽略,可能在后面的问产生问题]

第一问最大流:最大流就增广路径

第二问:因为要求,让最小割的被割边的数量也最小,在进行最大流后,对所有满载且原始边不为0的边容量改为1,残余改为inf,再进行一次求最大流

第三问:在第二问基础上枚举删边,检查可能性,标记边.在原图上采取枚举删边,

关于此题

有看到这样的解法来处理二三问,把所有边按容量降序排列依次删边尝试,据说能过USACO,然而有反例[USACO数据真弱2333]

1
2
3
4
5
6
3 5
1 2 1
1 2 1
1 2 4
2 3 3
2 3 3

很明显总流量是6,要让最小割的数量最小是点2到点3 的两条流量为3的边


据说,有人的方案是把边容量乘1001再加上边index来做为新容量,从而最小边取%1001,最大流取/1001


测试数据里似乎没有c=0的时候


第二问的操作inf,必要性,简单的反例解释

1
2
3
4
5
6
7
8
4 7
1 2 7
2 3 7
1 3 2
3 4 2
3 4 2
3 4 2
3 4 2

明显1到3的流量可以达到9,而3到4的流量只有8

所以最小割一定是3到4,但如果耗尽改为1的话,且1到2,2到3都被耗尽,则有1到2,2到3都改为流量1,进行做第二问

如果不把1到3的2 改为inf,则会 认为最少的割边数量为3,而不是4


同样第二问结果并不能直接用于第三问,进行尝试删边,样例

1
2
3
4
5
6
4 5
1 2 4
2 3 2
2 4 2
1 3 100
3 4 2

最小割应该为 2->4 和3->4,如果1->2耗尽,在第二问变更后的图做尝试删边操作,则会发现1->2会影响流量,认为1->2属于割边

代码

code

EDU55E

输入

给你n(1<=n<=500'000)个数字(1<=a[i]<=500'000)和 一个目标值 c(1<=c<=500'000)

要求

任选连续一段加上任意值,使最后的c的出现次数最多,

输出

c最多出现的次数

解法

假设选的区间[l,r]是顶着头,或者 抵着尾的,那么和明显

ans= min(max{[0,i)出现k数字的次数}+[i,n)出现c的次数,max{[i,n)出现k数字的次数}+[0,i)出现c的次数)

用前缀和,可以O(n)做出,问题是无法解决 原数组1 2 1 目标是1的这种,只需要改中间的部分的.


想法1是 做分治

f(l,r) = max(fl(l,mid)+fr(mid,r),max(f(l,mid),f(mid,r)))

fl: c...c?...?

fr: ?...?c...c

也就是划分的mid是否 在选取的区间[l,r]内,问题是 看似分治了,但fl(l,mid)+fr(mid,r) 无法处理替换段是一样的情况,如果要处理时间复杂度不会够


之后想法是dp

因为可以观察到如果选取的值为v,那么整个数组上统计出现个数的时候采取的是形如

c…cv…vc…c的形状(其中每个形状均可以长度为0),那么也就是可以dp[state][i]来做,state分为第一段 第二段 第三段,

这样看上去是n*m的,但是 实际上当我们走到i的时候,只有a[i]的dp需要更改,所以是O(n)的


dp的整理

既然上面也观察到进行到i,只会影响到a[i]相关,那么可以把不同数字的都整合到一起,因为只会有当前a[i]对i位置的进行写和读(==c的会有其它读)

考虑形状

1
2
c...c?...?c...c
i

那么有 ans = max{ [0,i] 中前部分选c后部分选a[i]的最大次数+[i+1,n-1]c出现的次数 }

1
2
3
[0,i]中前部分选c后部分选a[i]的最大次数
= [0,i]中a[i]的次数 + max{[0,j]选c相对于选a[i]的增量} 其中0<=j<=i
= [0,i]中a[i]的次数 + max{[0,j]选c-[0,j]选a[i]} 其中0<=j<=i

至此上面皆可前缀和

1
2
3
4
5
6
7
8
9
i = 1 -> n:
sumc[i] = sumc[i-1] + (a[i] == c); // 前缀和,求到i个 有多少个c
sumci[i] = sumci[pre[a[i]]]+1; // 前缀和,求到i个 有多少个a[i]
maxder[i]=max(maxder[pre[a[i]]],sumc[i-1]-sumv[i]+1); // [0->j] 计算a[i] -> c变化的最大增量
pre[a[i]]=i; // pre记录a[i]上一次出现的位置
}
ans <- 0;
i = 0 -> n;
ans=max(ans,maxder[i]+sumci[i]+(sumc[n]-sumc[i])); // 增量+[0,i]中a[i]的个数+[i+1,n-1]中c的个数

代码

code

0%