7.11 位运算专题
二进制中1的个数
笔记:Lowbit函数
假设 x 的原码是 011001000
-x在二进制里表示为 ~x+1
~x 表示为100110111
加一之后 100111000
再将二者进行 & 操作
结果为 1000 返回的为最后一个 1 开始的数
#include<iostream>
using namespace std;
int lowbit(int x){
return x&-x;
}
int main(){
int n; cin>>n;
while(n--){
int a,res=0;cin>>a;
while(a) {
a-=lowbit(a); res++;
}
cout<<res<<' ';
}
return 0;
}
笔记: 若想获取某个数字的第 n-1 位
首先右移n位 再与 1 进行 & 运算 结果无非是1 或 0
#include<iostream>
using namespace std;
long n,a;
int main(){
scanf("%ld %ld",&n,&a);
cout<<(n>>(a-1) & 1)<<endl;
return 0;
}
二进制与一
笔记:
当
n
n
n的第
k
k
k位不为 1 时,我们可以注意到,如果将
n
n
n对
2
k
−
1
2^{k-1}
2k−1取模,就可以得到
n
n
n的最末尾
k
−
1
k-1
k−1个二进制位的十进制结果
c
o
c_{\mathrm{o}}
co
此时,我们让
x
=
2
k
−
1
−
c
x=2^{k-1}-c
x=2k−1−c,即可让
n
+
x
=
n
−
c
+
2
k
−
1
n+x=n-c+2^{k-1}
n+x=n−c+2k−1的二进制第
k
k
k位变成 1,而
1
∼
k
−
1
1\sim k-1
1∼k−1位都变成0。这便是最小的
x
x
x的求法。
#include<iostream>
#define ll long long
using namespace std;
int main() {
ll m, n; cin >> m >> n;
ll sum = 0;
while (n--) {
ll k; cin >> k; --k; // 得到 k - 1
if (m & ( 1ll << k) ) continue; //验证第k位是否为 1
else { // 1ll = ((long long) 1)
// 计算低位值:获取m低于第k位的数值 (相当于m mod 2^k
ll c = m % (1ll << k);
// 更新m:清零低位并设置第k位为1
m = m - c + (1ll << k);
// 同步更新总和:减去原有低位的值,添加新的第k位值
sum = sum - c + (1ll << k);
}
}//将m和sum的第k位设为1,并且将低于第k位的所有位清零(即置为0)
cout << sum << endl;
return 0;
}
找筷子 (位运算的模拟题)
此题涉及大数据的读入
#include <iostream>
#include <cstdio>
using namespace std;
inline int read()
{
long long s = 0,f = 1;
char ch = getchar();
while((ch < '0' || ch > '9') && ch != EOF)
{
if(ch == '-') f = -1;
ch = getchar();
}
while(ch >= '0' && ch <= '9')
{
s = s * 10 + ch - '0';
ch = getchar();
}
return s * f;
}
int main()
{
int n,x,ans = 0;
n = read();
for(register int i = 1;i <= n;++i) x = read(),ans ^= x;
printf("%d\n",ans);
return 0;
}
7.12 离散化专题
离散化模板
vector<int> alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end()); // 去掉重复元素
// 二分求出x对应的离散化的值
int find(int x) // 找到第一个大于等于x的位置
{
int l = 0, r = alls.size() - 1;
while (l < r)
{
int mid = l + r >> 1;
if (alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1; // 映射到1, 2, ...n
}
区间和(涉及前缀和,二分,排序,唯一化等基础算法)
笔记:
离散化的本质是将数字本身key映射为它在数组中的索引index(1 based)。
所以通过二分求索引(value->index)是离散化的本质,建立了一段数列到自然数之间的映射关系
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 300010;
int a[N], s[N];
typedef pair<int, int> PII;//因为每个操作是先定位在修改数据 所以 用pair来存储位置和修改值
vector<int> alls;//alls是所有需要用到的下标,而不是单单那些非0的点的下标,
//在去重排序过后可以就是一个个有序但在数字上并不连续的下标
vector<PII> add;//add是需要修改的点 存放的是原数组下标和修改值
vector<PII> query;//query是需要查询的区间 存放的是原数组下标和修改值
int find(int x) {//查询原数组下标在伪数组的位置
int l = 0, r = alls.size() - 1;
while (l < r) {
//二分查找的过程就是将这些有序原下标转化为连续的1, 2, 3, 4, 5.....下标的过程,
// 这个下标是a数组的下标,a数组用来存储每次 + c的数据。
int mid = (l + r) >> 1;
if (alls[mid] >= x) r = mid;//从alls数组中找大于等于x的最小值 也就是x的下标 映射为伪数组的下标
else l = mid + 1;
}
return r + 1;//返回的是从1开始的伪数组下标 避免了边界情况的判断
}
int main() {
int n, m; cin >> n >> m;
for (int i = 1; i <= n; i++) {
int x, c; cin >> x >> c;
add.push_back({ x,c });
alls.push_back(x);
}
for (int i = 1; i <= m; i++) {
int l, r; cin >> l >> r;//存入l r断点的目的是为了将区间[l,r]映射为伪数组的下标 不一定会+C
query.push_back({ l,r });
alls.push_back(l); //alls数组就是用来将原下标进行离散化处理的,要是没有往里面存入l和r,
alls.push_back(r);//在离散过后我们就无法精确的找到l和r对应的a数组下标,也就是找不到这个区间
}
sort(alls.begin(), alls.end());//排序 对alls排序 去重,便于在后面查找时时间复杂度更低
alls.erase(unique(alls.begin(), alls.end()), alls.end());//去重 unique的本质是双指针算法
for (auto item : add) {
int x = find(item.first);//item.first为原数组的下标,通过find函数将其映射为伪数组的下标
a[x] += item.second;//a数组用来存储每次 + c的数据
}
for (int i = 1; i <= alls.size(); i++) s[i] = s[i - 1] + a[i];//用s数组来作为前缀数组 方便区间和的计算
for (auto item : query) {
int l = find(item.first); int r = find(item.second);//item.first和item.second为原数组的下标 映射为伪数组的下标
cout << s[r] - s[l - 1] << endl;
}
return 0;
}
7.13区间合并专题
区间化模板
// 将所有存在交集的区间合并
void merge(vector<PII> &segs)
{
vector<PII> res;
sort(segs.begin(), segs.end());
int st = -2e9, ed = -2e9;
for (auto seg : segs)
if (ed < seg.first)
{
if (st != -2e9) res.push_back({st, ed});
st = seg.first, ed = seg.second;
}
else ed = max(ed, seg.second);
if (st != -2e9) res.push_back({st, ed});
segs = res;
}
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N=100010;
int n;
typedef pair<int,int> PII;
vector<PII> segs;
void merge(vector<PII> &segs){
vector<PII> res;
sort(segs.begin(), segs.end());
int st = -2e9, ed = -2e9;
for (auto seg : segs)
if (ed < seg.first)
{
if (st != -2e9) res.push_back({st, ed});
st = seg.first, ed = seg.second;
}
else ed = max(ed, seg.second);
if (st != -2e9) res.push_back({st, ed});
segs = res;
}
int main(){
cin>>n;
for(int i=0;i<n;i++){
int l,r;
cin>>l>>r;
segs.push_back({l,r});
}
merge(segs);
cout<<segs.size()<<endl;
return 0;
}
7.14 --7.25 断更
这期间由于学校安排小学期的程序设计 没有练习算法 21号又去了清华大学进行交流学习 对于大模型 LLM CV NLP 等科研方向有了很大进步和资源整合 下面我先放出这段时间的项目工作
《狗了个狗 —— 基于 C++ 与 EasyX 的趣味三消游戏设计与实现》
源码仓库:游戏源项目
对于这款游戏的实现具体的我就不在这里赘述了
清华交流
差距极大 菜就多练
7.26 数据结构专题
初期这里并未使用 STL 容器 是使用数组模拟实现的 这样做的目的我在后面会阐述原因。
单链表
#include<iostream>
using namespace std;
const int N = 100010;
//head表示头节点的下标位置
//e[]数组存放数据
//ne[]数组存放下一个节点的下标位置
//idx表示目前已分配的节点个数
int head, e[N], ne[N], idx;
void Init() {//初始化
head = -1;
idx = 0;
}
void add_to_head(int x) {//头插
e[idx] = x, ne[idx] = head, head = idx++;
}
void add(int k, int x) {//插入
e[idx] = x, ne[idx] = ne[k], ne[k] = idx++;
}
void remove(int k) {
ne[k] = ne[ne[k]];
}
int main() {
int m; cin >> m;
Init();
while (m--) {
int k; int x;
char op; cin >> op;
if (op == 'H') {
cin >> x;
add_to_head(x);
}
else if (op == 'I') {
cin >> k >> x;
add(k - 1, x);
}
else {
cin >> k;
if (!k) head = ne[head];//删除头节点
else remove(k - 1);//删除第k次插入的节点的后面一个节点
}
}
for (int i = head; i != -1; i = ne[i]) cout << e[i] << " ";
cout << endl;
return 0;
}
双链表
#include<iostream>
using namespace std;
const int N = 100010;
int e[N], r[N], l[N], idx;
void Init() {
//0表示头节点 1表示尾节点
r[0] = 1; l[1] = 0;
idx = 2;
}
void insert(int k, int x) {
e[idx] = x, l[idx] = k, r[idx] = r[k], l[r[k]] = idx, r[k] = idx++;
}
void remove(int k) {
r[l[k]] = r[k], l[r[k]] = l[k];
}
int main() {
int m; cin >> m;
Init();
while (m--) {
int k, x; string op; cin >> op;
if (op == "L") {
cin >> x; insert(0, x);
}
else if (op == "R") {
cin >> x; insert(l[1], x);
}
else if (op == "D") {
cin >> k; remove(k + 1);//k-1(正常下标) +2(从2开始插入 故+2)
}
else if (op == "IL") {
cin >> k >> x; insert(l[k + 1], x);
}
else {
cin >> k >> x; insert(k + 1, x);
}
}
for (int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';
return 0;
}
栈和队列
用数组模拟这两个相对较简单
模拟栈
#include<iostream>
using namespace std;
const int N=100010;
int st[N];int top=-1;
int n;
int main(){
cin>>n;
while(n--){
string op; cin>>op;int x;
if(op=="push"){
cin>>x;
st[++top]=x;
}
else if(op=="pop") top--;
else if(op=="query"){
cout<<st[top]<<endl;
}
else{
cout<<( top ==-1 ? "YES ":"NO")<<endl;
}
}
return 0;
}
#include<iostream>
using namespace std;
const int N=100010;
int hh, q[N] ; int tt = -1;
int main(){
int m;cin>>m;
while(m--){
int x;
string op;cin>>op;
if(op=="push"){
cin>>x; q[++tt]=x;
}
else if(op=="pop") hh++;
else if(op=="query"){
cout<<q[hh]<<endl;
}
else {
cout<<(hh<=tt ? "NO":"YES")<<endl;
}
}
return 0;
}
单调栈
这种问题可能比较抽象 下面我先放出一个比较经典的问题
找最近的最小值

数组模拟法
#include <iostream>
using namespace std;
const int N = 100010;
int stk[N], tt;
int main()
{
int n;
cin >> n;
while (n -- )
{
int x;
scanf("%d", &x);
while (tt && stk[tt] >= x) tt -- ;//如果栈顶元素大于当前待入栈元素,则出栈
if (!tt) printf("-1 ");//如果栈空,则没有比该元素小的值。
else printf("%d ", stk[tt]);//栈顶元素就是左侧第一个比它小的元素。
stk[ ++ tt] = x;
}
return 0;
}
使用STL中的stack实现
#include<iostream>
#include<stack>
using namespace std;
stack<int> st;
int a[100010];
int main(){
int n;cin>>n;
for(int i=0;i<n;i++) cin>>a[i];
st.push(-1);
for(int i=0;i<n;i++){
while(st.top()>=a[i]) st.pop();
cout<<st.top()<<' ';
st.push(a[i]);
}
return 0;
}
单调队列
单调队列往往用于滑动窗口的优化中 以下我展示此类经典问题
滑动窗口 单调队列优化
使用数组模拟实现
#include<iostream>
using namespace std;
const int N = 1000010;
int n, k, a[N];
int q[N], hh, tt;
int main() {
cin >> n >> k;
for (int i = 1; i <= n; i++) cin >> a[i];
// 初始化队列
hh = 0, tt = -1;
for (int i = 1; i <= n; i++) {
// Q1:队列里面放的是什么?
// A1:是数组下标,是编号,不是值,需要值时,可以通过a[q[hh]]去取值
// Q2:队列的存入形态是什么样的?
// A2: hh....tt,hh在左,tt在右
// Q3:什么样的需要出队列?
// A3: (1)距离当前位置超过窗口范围
// (2) 在窗口范围内,但对后续没有可能发挥作用:区间内的人员,有比你更年轻、更漂亮的,怎么选美也选不到你
while (hh <= tt && i + 1 - k > q[hh]) hh++; // q[hh]:窗口的左起点,hh++:减小窗口长度
while (hh <= tt && a[q[tt]] >= a[i]) tt--; // tt--:减小窗口长度
// q[],hh,tt 三者组成了一个模拟的队列数据结构,对外提供:查询队列中最小值位置的服务q[hh],对内三者互相协作
// 换言之:q[hh]是对外的,hh,tt是数据结构内部概念,不能混淆
q[++tt] = i;
// 只有在区间长度够长的情况下,才能输出区间最小值
if (i >= k) printf("%d ", a[q[hh]]);
}
puts("");
hh = 0, tt = -1;
for (int i = 1; i <= n; i++) {
while (hh <= tt && i + 1 - k > q[hh]) hh++;
while (hh <= tt && a[q[tt]] <= a[i]) tt--;
q[++tt] = i;
if (i >= k) printf("%d ", a[q[hh]]);
}
return 0;
}
同样 我仍然给出
STL中deque解法
#include <iostream>
#include <cstring>
#include <algorithm>
#include <deque>
using namespace std;
const int N = 1000010;
int a[N];
int main()
{
int n, k;
cin >> n >> k;
for (int i = 1; i <= n; i ++ ) cin >> a[i];//读入数据
deque<int> q;
for(int i = 1; i <= n; i++)
{
while(q.size() && q.back() > a[i]) //新进入窗口的值小于队尾元素,则队尾出队列
q.pop_back();
q.push_back(a[i]);//将新进入的元素入队
if(i - k >= 1 && q.front() == a[i - k])//若队头是否滑出了窗口,队头出队
q.pop_front();
if(i >= k)//当窗口形成,输出队头对应的值
cout << q.front() <<" ";
}
q.clear();
cout << endl;
//最大值亦然
for(int i = 1; i <= n; i++)
{
while(q.size() && q.back() < a[i]) q.pop_back();
q.push_back(a[i]);
if(i - k >= 1 && a[i - k] == q.front()) q.pop_front();
if(i >= k) cout << q.front() << " ";
}
}
单链表,双链表,栈和队列这四个经典的数据结构 在这里我首选的是数组模拟实现
以链表为例子来说明
为什么不用课本上学的结构体来构造链表??
学过数据结构课的人,对链表的第一反应就是:
链表由节点构成,每个节点保存了 值 和 下一个元素的位置 这两个信息。节点的表示形式如下:
class Node{
public:
int val;
Node* next;
};
这样构造出链表节点的是一个好方法,也是许多人一开始就学到的。
使用这种方法,在创建一个值为 x 新节点的时候,语法是:
Node* node = new Node();
node->val = x
看一下创建新节点的代码的第一行:
Node* node = new Node();中间有一个 new 关键字来为新对象分配空间。
new的底层涉及内存分配,调用构造函数,指针转换等多种复杂且费时的操作。一秒大概能new 1w次左右。
在平时的工程代码中,不会涉及上万次的new操作,所以这种结构是一种 见代码知意 的好结构。
但是在算法比赛中,经常碰到操作在10w级别的链表操作,如果使用结构体这种操作,是无法在算法规定时间完成的。
所以,在算法比赛这种有严格的时间要求的环境中,不能频繁使用new操作。也就不能使用结构体来实现数组。
同时 我也尝试分别提交数组模拟和STL实现的两种解法的运行时间对比
这是 单调栈 那一题的 时间对比 上为 STL 下为数组模拟 时间相差两倍

这是 单调队列 - 滑动窗口 那一题的 时间对比 下为 STL 上为数组模拟 时间相差三倍左右

7.27 KMP
kmp 属于串里面很经典的一个问题
我的大学老师有一篇很清晰的博客 感兴趣的可以阅读一下 看完一定会对kmp这个专题有更明细的理解 KMP 详细讲解
下面我给出一些模板题目
KMP匹配
这道题是已知模式串和文本串的大小长度 处理起来使用静态数组即可
#include<iostream>
using namespace std;
const int N=100010,M=1000010;
int n,m,ne[N];
char p[N],s[M];
int main(){
cin>>n>>p+1>>m>>s+1;
//求next
for(int i=2,j=0;i<=n;i++){
while(j && p[i]!=p[j+1]) j=ne[j];
if(p[i]==p[j+1]) j++;
ne[i]=j;
}
//kmp匹配
for(int i=1,j=0;i<=m;i++){
while(j&& s[i]!=p[j+1]) j=ne[j];
if(s[i] == p[j+1]) j++;
if(j==n) {
printf("%d ",i-n);
j=ne[j];
}
}
return 0;
}
当然 当我们处理未知长度的串时 常常使用STL vector动态数组处理
未知长度KMP匹配
#include<iostream>
#include<vector>
#include<string>
using namespace std;
vector<int> ans;
string s, p;
int main() {
getline(cin, s); getline(cin, p);
s = ' ' + s; p = ' ' + p;
int m = s.size(), n = p.size();
vector<int> ne(n + 1, 0);
ne[0] = -1;
for (int i = 2, j = 0; i <= n; i++) {
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j++;
ne[i] = j ;
}
for (int i = 1, j = 0; i <= m; i++) {
while (j && s[i] != p[j + 1])j = ne[j];
if (s[i] == p[j + 1]) j++;
if (j == n - 1) {
ans.push_back(i - n + 2);
j = ne[j];
}
}
cout << ans.size() << endl;
for (auto pos : ans) cout << pos << endl;
for (int i = 0; i < n-1; i++) cout << ne[i] << ' ';
return 0;
}
Trie树
通常Trie树用于大量字符串的处理查询删除等操作
仔细理解这种结构后会发现
这也是一个典型的空间换时间的数据结构


#include<iostream>
using namespace std;
const int N=100010;
int son[N][26],idx,cnt[N];
char str[N];
void insert(char str[]){
int p=0;
for(int i=0;str[i];i++){
int u=str[i]-'a';
if(!son[p][u]) son[p][u]=++idx;
p=son[p][u];
}
cnt[p]++;
}
int query(char str[]){
int p=0;
for(int i=0;str[i];i++){
int u=str[i]-'a';
if(!son[p][u]) return 0;
p=son[p][u];
}
return cnt[p];
}
int main(){
int n; cin>>n;
while(n--){
char op[2]; scanf("%s%s",op,str);
if(op[0]=='I') insert(str);
else printf("%d\n",query(str));
}
return 0;
}
并查集
对于并查集这个本身是比较简单的 一些简单的操作即可理解 虽然它的代码量很小 但是其内部所蕴含的算法思想还是很高效有趣的 并且 并查集常常被运用到其他算法优化中 其重要性和灵活度都是很高的
下面我先展示一个初级的并查集算法题 各位可以感受一下
并查集模板题
#include <iostream>
using namespace std;
const int N = 100010;
int p[N];
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) p[i] = i;
while (m -- )
{
char op[2];
int a, b;
scanf("%s%d%d", op, &a, &b);
if (*op == 'M') p[find(a)] = find(b);
else
{
if (find(a) == find(b)) puts("Yes");
else puts("No");
}
}
return 0;
}
7.28 KMP续集
对于昨天的KMP 我再给出一道例题
旨在让读者明白 在基础算法根据题意改编时 通过额外维护几个数组或变量 就可以记录到相对应的数量
连通块的数量
#include<iostream>
using namespace std;
const int N=100010;
int m,n,p[N];
int sz[N];
int find(int x){
if(p[x]!=x) p[x]=find(p[x]) ;
return p[x];
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) {
p[i]=i; sz[i]=1;
}
while(m--){
char op[5]; scanf("%s",op);
int a,b;
if(op[0]=='C'){
scanf("%d%d",&a,&b);
if(find(a)==find(b)) continue;
sz[find(a)]+=sz[find(b)];
p[find(b)]=find(a);
}
else if(op[1]=='1'){
scanf("%d%d",&a,&b);
if(find(a)==find(b)) puts("Yes");
else puts("No");
}
else {
scanf("%d",&a);
printf("%d\n",sz[find(a)]);
}
}
return 0;
}
堆
这里主要是使用数组来模拟实现堆的操作
通常 先使用down() 和 up() 的两个函数就可以实现对一系列数据的处理

首先我先给出一个较为经典的模板题吧
堆排序
#include<iostream>
#include<algorithm>
using namespace std;
const int N=100010;
int n,m,a[N],r;
void down(int n){
int t=n;//t记录最小点的编号
if(2*n <=r && a[2*n]<a[ n ]) t=2*n;//有左儿子,并且左儿子比t节点的值小,更新t
if(2* n +1<=r && a[2*n+1]<a[t]) t=2* n+1;//有右儿子,并且右儿子比t节点的值小,更新t
if(t!=n){
swap(a[t],a[n]);
down(t);
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
r=n;
for(int i=n/2; i;i--) down(i);
while(m--){
printf("%d ",a[1]);
a[1]=a[r];
r--;
down(1);
}
return 0;
}


堆排序的相关概念补充拓展
- 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序, 它的最坏、最好、平均时间复杂度均为 O(nlogn), 它也是不稳定排序。
- 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值, 这种情况称为大顶堆,注意:没有要求结点的左孩子的值和右孩子的值的大小关系。
- 每个结点的值都小于或等于其左右孩子结点的值, 这种情况称为小顶堆。
- 大顶堆举例说明:

- 小顶堆举例说明:

- 如果要求排序为升序,一般采用大顶堆,每次将堆顶元素与未排序的最后一个元素交换,再调整堆。
- 如果要求排序为降序,一般采用小顶堆,每次将堆顶元素与未排序的最后一个元素交换,再调整堆。
数组模拟堆的操作例题
#include<iostream>
#include<algorithm>
#include <string.h>
using namespace std;
const int N=100010;
int m,a[N],r,idx;
int hp[N]; //堆中序号和输入序号的映射关系 hp[2]=3 堆中排在第2的是第3个输入的
int ph[N];//输入序号和堆中序号的映射关系 ph[3]=2 第3个输入的在堆中排在第2个
// 交换两个堆中的元素(p,q是指堆中序号)
void Swap(int p,int q){
swap(a[p],a[q]);// 交换堆中序号p与序号q的元素值
swap(hp[p],hp[q]);// 输入序号交换
swap(ph[hp[p]],ph[hp[q]]);
}
void down(int u){
int t=u;
if(2*u<=r&&a[2*u]<a[u]) t=2*u;
if(2*u+1<=r&& a[2*u+1]<a[t]) t=2*u+1;
if(t!=u){
Swap(u,t);
down(t);
}
}
void up(int u){
while(u/2&&a[u/2]>a[u]){ // 爹活着,比爹牛,上位!(玄武门事变?)
Swap(u/2,u);
u/=2;
}
}
int main(){
scanf("%d",&m);
r=0;
while(m--){
int p,q;char op[5]; scanf("%s",op);
if(!strcmp(op,"I")){
scanf("%d",&p);
idx++; r++;
hp[r]=idx; ph[idx]=r;
a[r]=p;
up(r);
}
else if(!strcmp(op,"PM")) printf("%d\n",a[1]);
else if(!strcmp(op,"DM")){
Swap(1,r);
r--;
down(1);
}
else if(!strcmp(op,"D")) {
scanf("%d",&p);
p=ph[p];
Swap(p,r);
r--;
down(p);up(p);
}
else {
scanf("%d%d",&p,&q);
p=ph[p];
a[p]=q;
down(p);
up(p);
}
}
return 0;
}
这道题相对而言比较难懂 注意题目中要求 是对第k次插入进去的数进行操作 所以需要额外开两个数组进行维护 那么里面的映射关系相对较复杂 不过这样的题目多见多看多研究后 下次遇到也就简单了
这里面有一个 idx 变量在我数据结构专题里面出现了很多次
这里我对这个idx也进行一些专项探讨和研究
如何理解单(双)链表,Trie树和堆中的idx
可以看出不管是链表,Trie树还是堆,他们的基本单元都是一个个结点连接构成的,可以成为“链”式结构。这个结点包含两个基本的属性:**本身的值和指向下一个结点的指针。**按道理,应该按照结构体的方式来实现这些数据结构的,但是做算法题一般用数组模拟,主要是因为比较快。
那就有个问题,原来这两个属性都是以结构体的方式联系在一起的,现在如果用数组模拟,如何才能把这两个属性联系起来呢,如何区分各个结点呢?
这就需要用到idx这个东东啦!
从y总给出的代码可以看出,idx的操作总是idx++,这就保证了不同的idx值对应不同的结点,这样就可以利用idx把结构体内两个属性联系在一起了。因此,idx可以理解为结点。
链表:
链表中会使用到这几个数组来模拟:
head存储链表头指向的节点,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪
个节点 h, e[N], ne[N], idx;
h表示头结点指针,一开始初始化指向-1,每次插入x的操作idx++。利用idx联系结构体本身的值和next指针,因此e[idx ]可以作为结点的值,ne[idx] 可以作为next指针。同理可以理解双链表。
//单链表
void add_to_head (int x)
{
e[idx] = x;
ne[idx] = h;
h = idx ++ ;
}
//双链表
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针
//,idx表示当前用到了哪个节点
void insert(int a, int x)
{
e[idx] = x;
l[idx] = a;
r[idx] = r[a];
l[r[a]] = idx;
r[a] = idx ++;
}
Trie树
Trie树中有个二维数组 son[N][26],表示当前结点的儿子,如果没有的话,可以等于++idx。Trie树本质上是一颗多叉树,对于字母而言最多有26个子结点。所以这个数组包含了两条信息。比如:son[1][0]=2表示1结点的一个值为a的子结点为结点2;如果son[1][0] = 0,则意味着没有值为a子结点。这里的son[N][26]相当于链表中的ne[N]。
void insert(char str[])
{
int p = 0; //从根结点开始遍历
for (int i = 0; str[i]; i ++ )
{
int u =str[i] - 'a';
if (!son[p][u]) son[p][u] = ++ idx; //没有该子结点就创建一个
p = son[p][u]; //走到p的子结点
}
cnt[p] ++; // cnt相当于链表中的e[idx]
}
堆
堆中的每次插入都是在堆尾,但是堆中经常有up和down操作。所以结点与结点的关系并不是用一个ne[idx][2]可以很好地维护的。但是好在堆是个完全二叉树。子父节点的关系可以通过下标来联系(左儿子2n,右儿子2n+1)。就数组模拟来说,知道数组的下标就知道结点在堆中的位置。所以核心就在于即使有down和up操作也能维护堆数组的下标(k)和结点(idx)的映射关系。 比如说:h[k] = x, h数组存的是结点的值,按理来说应该h[idx]来存,但是结点位置总是在变的,因此维护k和idx的映射关系就好啦,比如说用ph数组来表示ph[idx] = k, 那么结点值为h[ph[idx]], 儿子为ph[idx] * 2和ph[idx] * 2 + 1, 这样值和儿子结点不就可以通过idx联系在一起了吗?
if (op == "I")
{
scanf("%d", &x);
size ++ ;
idx ++ ;
ph[idx] = size, hp[size] = idx;//每次插入都是在堆尾插入
h[size] = x;//h[k], k是堆数组的下标,h存储的是结点的值,也就是链表中的e[idx]
up(size);
}
由于idx只有在插入的时候才会更新为idx ++,自然idx也表示第idx插入的元素
7.29 哈希表
对于哈希表的存储方式 我们通常我们使用两种存储方式
- 拉链法
- 开放寻址法
//拉链法 (了解模拟单链表的操作原理
#include<cstring>
#include<iostream>
using namespace std;
const int N = 100003;
int h[N], e[N], ne[N], idx;
void Insert(int x) {//拉链哈希表的插入操作
int k = (x % N + N) % N;//加N又模N的目的是保证余数为正
e[idx] = x;
ne[idx] = h[k];
h[k] = idx++;
}
bool Query(int x) {//拉链哈希表的查询操作
int k = (x % N + N) % N;
for (int i = h[k]; i != -1; i = ne[i]) {
if (e[i] == x) return true;
}
return false;
}
int main() {
int n; scanf("%d", &n);
memset(h, -1, sizeof h);
while (n--) {
char op[2]; int x; scanf("%s%d", &op, &x);
if (*op == 'I') Insert(x);
else if (Query(x)) puts("Yes");
else puts("No");
}
return 0;
}
//开放寻址法(蹲坑法
#include<iostream>
#include<cstring>
using namespace std;
const int N = 200003, null = 0x3f3f3f3f;
int h[N];
int find(int x) {
int k = (x % N + N) % N;
while (h[k] != null && h[k] != x) {
k++;
if (k == N) k = 0;
}
return k;
}
int main() {
int n; scanf("%d", &n);
memset(h, 0x3f, sizeof h);
while (n--) {
char op[2]; int x;
scanf("%s%d", op, &x);
int k = find(x);
if (*op == 'I') h[k] = x;
else {
if (h[k] != null) puts("Yes");
else puts("No");
}
}
return 0;
}
哈希表在字符串的处理里也表现出很高效的匹配的操作
可以实现除KMP以外其他的功能
字符串哈希
全称字符串前缀哈希法,把字符串变成一个p进制数字(哈希值),实现不同的字符串映射到不同的数字。
对形如 X1X2X3⋯Xn−1Xn 的字符串,采用字符的ASCII 码乘上 P 的次方来计算哈希值。
映射公式:
(
X
1
×
P
n
−
1
+
X
2
×
P
n
−
2
+
⋯
+
X
n
−
1
×
P
1
+
X
n
×
P
0
)
m
o
d
Q
(X_1\times P^{n-1}+X_2\times P^{n-2}+\cdots+X_{n-1}\times P^1+X_n\times P^0)\mathrm{~mod~}Q
(X1×Pn−1+X2×Pn−2+⋯+Xn−1×P1+Xn×P0) mod Q
前缀和公式
h
[
i
+
1
]
=
h
[
i
]
×
P
+
s
[
i
]
i
∈
[
0
,
n
−
1
]
h[i+1]=h[i]\times P+s[i]\begin{array}{c}i\in[0,n-1]\end{array}
h[i+1]=h[i]×P+s[i]i∈[0,n−1] h为前缀和数组,s为字符串数组 区间和公式
h
[
l
,
r
]
=
h
[
r
]
−
h
[
l
−
1
]
×
P
r
−
l
+
1
h[l,r]=h[r]-h[l-1]\times P^{r-l+1}
h[l,r]=h[r]−h[l−1]×Pr−l+1
注意点:
- 任意字符不可以映射成0,否则会出现不同的字符串都映射成0的情况,比如A,AA,AAA皆为0
- 冲突问题:通过巧妙设置P (131 或 13331) , Q (264)
问题是比较不同区间的子串是否相同,就转化为对应的哈希值是否相同。
求一个字符串的哈希值就相当于求前缀和,求一个字符串的子串哈希值就相当于求部分和。
区间和公式的理解: ABCDE 与 ABC 的前三个字符值是一样,只差两位,
乘上
P
2
P^{2}
P2 把 ABC 变为 ABC00,再用 ABCDE - ABC00 得到 DE 的哈希值。
#include<iostream>
#include<algorithm>
using namespace std;
typedef unsigned long long ULL;
const int N=100010,P=131;
int n,m;
char str[N];
ULL h[N],p[N];
// h[i]前i个字符的hash值
// 字符串变成一个p进制数字,体现了字符+顺序,需要确保不同的字符串对应不同的数字
// P = 131 或 13331 Q=2^64,在99%的情况下不会出现冲突
// 使用场景: 两个字符串的子串是否相同
ULL get(int l,int r){
return h[r]-h[l-1]*p[r-l+1];
}
int main(){
scanf("%d%d%s",&n,&m,str+1);
p[0]=1;
for(int i=1;i<=n;i++){
h[i]=h[i-1]*P+str[i];
p[i]=p[i-1]*P;
}
while(m--){
int l1,r1,l2,r2;
scanf("%d%d%d%d",&l1,&r1,&l2,&r2);
if(get(l1,r1)==get(l2,r2)) puts("Yes");
else puts("No");
}
return 0;
}
7.30 C++ STL介绍
vector, 变长数组,倍增的思想
size() 返回元素个数
empty() 返回是否为空
clear() 清空
front()/back()
push_back()/pop_back()
begin()/end()
[]
支持比较运算,按字典序
pair<int, int>
first, 第一个元素
second, 第二个元素
支持比较运算,以first为第一关键字,以second为第二关键字(字典序)
string,字符串
size()/length() 返回字符串长度
empty()
clear()
substr(起始下标,(子串长度)) 返回子串
c_str() 返回字符串所在字符数组的起始地址
queue, 队列
size()
empty()
push() 向队尾插入一个元素
front() 返回队头元素
back() 返回队尾元素
pop() 弹出队头元素
priority_queue, 优先队列,默认是大根堆
size()
empty()
push() 插入一个元素
top() 返回堆顶元素
pop() 弹出堆顶元素
定义成小根堆的方式:priority_queue<int, vector<int>, greater<int>> q;
stack, 栈
size()
empty()
push() 向栈顶插入一个元素
top() 返回栈顶元素
pop() 弹出栈顶元素
deque, 双端队列
size()
empty()
clear()
front()/back()
push_back()/pop_back()
push_front()/pop_front()
begin()/end()
[ ]
set, map, multiset, multimap, 基于平衡二叉树(红黑树),动态维护有序序列
size()
empty()
clear()
begin()/end()
++, -- 返回前驱和后继,时间复杂度 O(logn)
set/multiset
insert() 插入一个数
find() 查找一个数
count() 返回某一个数的个数
erase()
(1) 输入是一个数x,删除所有x O(k + logn)
(2) 输入一个迭代器,删除这个迭代器
lower_bound()/upper_bound()
lower_bound(x) 返回大于等于x的最小的数的迭代器
upper_bound(x) 返回大于x的最小的数的迭代器
map/multimap
insert() 插入的数是一个pair
erase() 输入的参数是pair或者迭代器
find()
[] 注意multimap不支持此操作。 时间复杂度是 O(logn)
lower_bound()/upper_bound()
unordered_set, unordered_map, unordered_multiset, unordered_multimap, 哈希表
和上面类似,增删改查的时间复杂度是 O(1)
不支持 lower_bound()/upper_bound(), 迭代器的++,--
bitset, 圧位
bitset<10000> s;
~, &, |, ^
>>, <<
==, !=
[]
count() 返回有多少个1
any() 判断是否至少有一个1
none() 判断是否全为0
set() 把所有位置成1
set(k, v) 将第k位变成v
reset() 把所有位变成0
flip() 等价于~
flip(k) 把第k位取反
7.31 数论(一)
质数
对于质数而言 主要的考点在于怎么去求他的质因数有哪些 该怎么去求这些质因数
质数定义:一个大于1的自然数,除了1和它本身外,不能被其他自然数整除,换句话说就是该数除了1和它本身以外不再有其他的因数,这个数就是质数。
试错法求质数
#include<iostream>
#include<algorithm>
using namespace std;
//试除法 时间复杂度是O(sqrt(n))
bool check(int x){
if(x < 2) return false;
for(int i=2;i<=x/i;i++)
if(x%i==0) return false;
return true;
}
int main(){
int n; scanf("%d",&n);
while(n--){
int x; scanf("%d",&x);
if(check(x)) puts("Yes");
else puts("No");
}
return 0;
}
质因数
#include<iostream>
#include<algorithm>
using namespace std;
//时间复杂度在 logN到sqrt(n)之间
void divide(int x){
for(int i=2;i<=x/i;i++){
if(x%i==0){//i一定是质数
int s=0;
while(x%i==0){
x/=i;
s++;
}
printf("%d %d\n",i,s);
}
}
if(x>1) printf("%d %d\n",x,1);
puts("");
}
int main(){
int n;scanf("%d",&n);
while(n--){
int x;scanf("%d",&x);
divide(x);
}
return 0;
}
(1).特别要注意------分解质因数与质因数不一样!!!
(2).分解质因数是一个过程,而质因数是一个数.
(3).一个合数分解而成的质因数最多只包含一个大于sqrt(n)的质因数
(反证法,若n可以被分解成两个大于sqrt(n)的质因数,则这两个质因数相乘的结果大于n,与事实矛盾).
(4).当枚举到某一个数i的时候,n的因子里面已经不包含2-i-1里面的数,
如果n%i==0,则i的因子里面也已经不包含2-i-1里面的数,因此每次枚举的数都是质数.
(5).算数基本定理(唯一分解定理):任何一个大于1的自然数N,如果N不为质数,那么N可以唯一分解成有限个质数的乘积
N=P1a1P2a2P3a3…Pnan,这里P1<P2<P3…<Pn均为质数,其中指数ai是正整数。
这样的分解称为 N 的标准分解式。最早证明是由欧几里得给出的,由陈述证明。
此定理可推广至更一般的交换代数和代数数论。
(6).质因子(或质因数)在数论里是指能整除给定正整数的质数。根据算术基本定理,不考虑排列顺序的情况下,
每个正整数都能够以唯一的方式表示成它的质因数的乘积。
(7).两个没有共同质因子的正整数称为互质。因为1没有质因子,1与任何正整数(包括1本身)都是互质。
(8).只有一个质因子的正整数为质数。
对于在一个范围内所有质数的筛选 我们需要严格讨论一下
范围内质数筛选
筛选质数
这道题的详细题解题解
1.朴素做法
1).做法:把2~(n-1)中的所有的数的倍数都标记上,最后没有被标记的数就是质数.
(2).原理:假定有一个数p未被2(p-1)中的数标记过,那么说明,不存在2(p-1)中的任何一个数的倍数是p,
也就是说p不是2(p-1)中的任何数的倍数,也就是说2(p-1)中不存在p的约数,因此,根据质数的定义可知:
p是质数.
(3).调和级数:当n趋近于正无穷的时候,1/2+1/3+1/4+1/5+…+1/n=lnn+c.(c是欧阳常数,约等于0.577左右.).
(4).底数越大,log数越小
(5).时间复杂度:约为O(nlogn);(注:此处的log数特指以2为底的log数).
int primes[N], cnt; // primes[]存储所有素数
bool st[N]; // st[x]存储x是否被筛掉
void get_prime(int n)
{
for(int i = 2; i <= n; i ++)
{
if(!st[i]) primes[cnt ++] = i;
for(int j = i + i; j <= n; j += i)
st[j] = true;
}
}
2.埃式算法
(1).质数定理:1~n中有n/lnn个质数.
(2).原理:在朴素筛法的过程中只用质数项去筛.
(3).时间复杂度:粗略估计:O(n).实际:O(nlog(logn)).
(4).1~n中,只计算质数项的话,”1/2+1/3+1/4+1/5+…+1/n”的大小约为log(logn).
int primes[N], cnt; // primes[]存储所有素数
bool st[N]; // st[x]存储x是否被筛掉
void get_primes(int n)
{
for (int i = 2; i <= n; i ++ )
{
if (st[i]) continue;
primes[cnt ++ ] = i;
for (int j = i + i; j <= n; j += i)
st[j] = true;
}
}
3.线性筛法
(1).若n在10的6次方的话,线性筛和埃氏筛的时间效率差不多,若n在10的7次方的话,线性筛会比埃氏筛快了大概一倍.
(2).思考:一:线性筛法为什么是线性的?
二:线性筛法的原理是什么?
(3).核心:1~n内的合数p只会被其最小质因子筛掉.
(4).原理:1~n之内的任何一个合数一定会被筛掉,而且筛的时候只用最小质因子来筛,
然后每一个数都只有一个最小质因子,因此每个数都只会被筛一次,因此线性筛法是线性的.
(5).枚举到i的最小质因子的时候就会停下来,即”if(i%primes[j]==0) break;”.
(6).因为从小到大枚举的所有质数,所以当”i%primes[j]!=0”时,primes[j]一定小于i的最小质因子,
primes[j]一定是primes[j]i的最小质因子.
(7).因为是从小到大枚举的所有质数,所以当”i%primes[j]==0”时,primes[j]一定是i的最小质因子,
而primes[j]又是primes[j]的最小质因子,因此primes[j]是iprimes[j]的最小质因子.
int primes[N], cnt; // primes[]存储所有素数
bool st[N]; // st[x]存储x是否被筛掉
void get_primes(int n)
{
for (int i = 2; i <= n; i ++ )
{
if (!st[i]) primes[cnt ++ ] = i;
for (int j = 0; primes[j] <= n / i; j ++ )
{
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
(8).关于for循环的解释:
注:首先要把握住一个重点:我们枚举的时候是从小到大枚举的所有质数
1.当i%primes[j]==0时,因为是从小到大枚举的所有质数,所以primes[j]就是i的最小质因子,而primes[j]又是其本身
primes[j]的最小质因子,因此当i%primes[j]==0时,primes[j]是primes[j]i的最小质因子.
2.当i%primes[j]!=0时,因为是从小到大枚举的所有质数,且此时并没有出现过有质数满足i%primes[j]==0,
因此此时的primes[j]一定小于i的最小质因子,而primes[j]又是其本身primes[j]的最小质因子,
所以当i%primes[j]!=0时,primes[j]也是primes[j]i的最小质因子.
3.综合1,2得知,在内层for循环里面无论何时,primes[j]都是primes[j]i的最小质因子,因此”st[primes[j]i]=true”
语句就是用primes[j]i这个数的最小质因子来筛掉这个数.
约数
对于约数:如果一个数a除以另一个数b的余数为0,即 a%b == 0, 则b是a的约数。
试错法求约数
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
vector<int> get_division(int x){
vector<int> a;
for(int i=1;i<=x/i;i++){
if(x%i==0){
a.push_back(i);
if(i!=x/i) a.push_back(x/i);
}
}
sort(a.begin(),a.end());
return a;
}
int main(){
int n;scanf("%d",&n);
while(n--){
int x; scanf("%d",&x);
vector<int> b=get_division(x);
for(int i=0;i<b.size();i++) printf("%d ",b[i]);
puts("");
}
return 0;
}
约数个数 约数之和
N
=
∏
i
=
1
k
p
i
a
i
=
p
1
a
1
⋅
p
2
a
2
⋯
p
k
a
k
约数个数:
∏
i
=
1
k
(
a
i
+
1
)
=
(
a
1
+
1
)
(
a
2
+
1
)
⋯
(
a
k
+
1
)
约数之和:
∏
i
=
1
k
(
∑
j
=
0
a
i
p
i
j
)
=
∏
i
=
1
k
(
p
i
0
+
p
i
1
+
⋯
+
p
i
a
i
)
=
(
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
)
\begin{aligned} N &= \prod_{i=1}^{k} p_i^{a_i} = p_1^{a_1} \cdot p_2^{a_2} \cdots p_k^{a_k} \\ \text{约数个数:} \quad & \prod_{i=1}^{k} (a_i + 1) = (a_1+1)(a_2+1) \cdots (a_k+1) \\ \text{约数之和:} \quad & \prod_{i=1}^{k} \left( \sum_{j=0}^{a_i} p_i^{j} \right) \\ &= \prod_{i=1}^{k} \left( p_i^{0} + p_i^{1} + \cdots + p_i^{a_i} \right) \\ &= \left( p_1^{0} + p_1^{1} + \cdots + p_1^{a_1} \right) \left( p_2^{0} + p_2^{1} + \cdots + p_2^{a_2} \right) \cdots \left( p_k^{0} + p_k^{1} + \cdots + p_k^{a_k} \right) \end{aligned}
N约数个数:约数之和:=i=1∏kpiai=p1a1⋅p2a2⋯pkaki=1∏k(ai+1)=(a1+1)(a2+1)⋯(ak+1)i=1∏k(j=0∑aipij)=i=1∏k(pi0+pi1+⋯+piai)=(p10+p11+⋯+p1a1)(p20+p21+⋯+p2a2)⋯(pk0+pk1+⋯+pkak)
约数个数
#include<iostream>
#include<unordered_map>
using namespace std;
typedef long long LL;
const int mod=1e9+7;
int main(){
int n;cin>>n;
unordered_map<int,int> primes;
//首先unordered_map是哈希表,该题数据量太大,
//而哈希表恰好可以提高访问单个数据的效率。同时unordered_map是可以存放pair类型的数据,
//它的first存储的是质数,他的second存储的是个数,遍历时速度会更快
while(n--){
int x;cin>>x;
for(int i=2;i<=x/i;i++){
while(x%i==0){
x/=i;
primes[i]++;
//Q1: primes[i]++; 这里加加的是map的first吗?
//A1:不是,这里加加的是map的value(值),而不是key(键)。
// 在 unordered_map<int, int> primes 中:
// primes 存储的是键值对(key-value pairs),其中:
// key(即 first)是质因数(整数类型)。
// value(即 second)是该质因数的指数(出现次数,整数类型)。
// 当你使用 primes[i] 时:
// 如果键 i 不存在,unordered_map 会自动插入一个新的键值对,键为 i,值初始化为 0(因为 int 类型默认初始化是 0)。
// primes[i]++ 相当于增加这个键对应的值(value),即增加质因数 i 的指数(计数)。
// 例如,如果 i = 2 且 primes[2] 原本是 0,执行后变为 1;如果再次执行,会变为 2,依此类推。
// 所以,这里操作的是 second(值),而不是 first(键)。
}
}
if(x>1) primes[x]++;
}
LL res=1;
for(auto prime:primes) res=res*(prime.second+1)%mod;
// Q2:这里只会列出不是0的那些质数吗?
// A2:是的,这里只会列出指数(value)不为0 的质因数(key)。
// 原因:
// 在质因数分解过程中,只有当某个质因数实际出现时,才会执行 primes[i]++ 或 primes[x]++。
// 例如,在 for(int i=2;i<=x/i;i++) 循环中,只有当 x % i == 0(即 i 是质因数)时,才会进入 while 循环并增加 primes[i]。
// 同样,在 if(x>1) primes[x]++; 中,只有当 x > 1(且 x 是质数)时,才会增加 primes[x]。
// 如果某个质因数从未出现(即指数为0),它不会被插入到 primes map 中。因为 unordered_map 只在通过 operator[] 或 insert 等操作访问时才会创建条目,
// 而在这个代码中,我们只在增加指数时才访问(如 primes[i]++),所以所有在 map 中的条目都有指数至少为 1。
// 在 for(auto prime:primes) 循环中:
// 由于 map 中只包含指数大于等于 1 的条目,所以循环只会遍历那些实际出现过的质因数(指数不为0)。
// 例如,如果输入数字是 12(质因数分解为 2^2 * 3^1),那么 primes map 中只有键 2 和 3,对应的值分别是 2 和 1。不会出现其他质因数(如 5、7 等),因为它们的指数为0,不在 map 中。
cout<<res<<endl;
return 0;
}
#include<iostream>
#include<unordered_map>
using namespace std;
typedef long long LL;
const int mod=1e9+7;
int main(){
int n;cin>>n;
unordered_map<int,int> primes;
while(n--){
int x;cin>>x;
for(int i=2;i<=x/i;i++){
while(x%i==0){
x/=i;
primes[i]++;
}
}
if(x>1) primes[x]++;
}
LL res=1;
for(auto prime:primes) {
int a=prime.first,b=prime.second;
LL t=1;
while(b--) t=(t*a+1)%mod;//可以转换成快速幂算法
res=res*t%mod;
}
cout<<res<<endl;
return 0;
}
对于为什么这里两次进行取模 我单独讨论一下

最大公约数(欧几里得算法
最大公约数算法
核心: gcd(a,b)=gcd(b, a mod b);
1.直接使用algorithm的_gcd算法 直接求出最大公约数 但这个方法我并不推荐
#include<iostream>
#include<algorithm>
using namespace std;
int main(void){
int t;
cin >> t;
while(t--){
int a, b;
cin >> a >> b;
cout << __gcd(a, b);
}
}
2.核心方法
#include<iostream>
using namespace std;
int gcd(int a,int b){
return b?gcd(b,a%b):a;//欧几里得算法核心 gcd(a,b)=gcd(b,a mod b);
}
int main(){
int n;cin>>n;
while(n--){
int x,y; cin>>x>>y;
cout<<gcd(x,y)<<endl;
}
return 0;
}
8.1 数论(二)
欧拉函数
欧拉函数 φ(n) 是定义在正整数 n 上的函数,表示小于或等于 n 的正整数中与 n 互质的数的个数。下面我们来推导欧拉函数的性质及其计算公式。
欧拉函数的性质
- 若 n 是质数 p,则 φ§ = p - 1。
这是因为除了 1 以外,其他所有小于 p 的正整数都与 p 互质。 - 若 n = p^k,其中 p 是质数,则 φ(p^k) = p^k - p^(k-1)。
这是因为小于 p^k 的正整数中,能被 p 整除的数有 p^(k-1) 个(即 p, 2p, …, p^(k-1)p),所以与 p^k 互质的数有 p^k - p^(k-1) 个。 - 欧拉函数是积性函数:若 m, n 互质,则 φ(mn) = φ(m)φ(n)。
这个性质是欧拉函数的核心性质,也是推导其计算公式的基础。
欧拉函数的计算公式推导
对于任意正整数 n,我们可以将其质因数分解为 n = p_1(e_1)p_2(e_2)…p_k^(e_k),其中 p_1, p_2, …, p_k 是不同的质数,e_1, e_2, …, e_k 是正整数。
利用欧拉函数的积性性质,我们有:
φ
(
n
)
=
φ
(
p
1
e
1
p
2
e
2
…
p
k
e
k
)
=
φ
(
p
1
e
1
)
φ
(
p
2
e
2
)
…
φ
(
p
k
e
k
)
φ(n) = φ(p_1^{e_1}p_2^{e_2}…p_k^{e_k}) = φ(p_1^{e_1})φ(p_2^{e_2})…φ(p_k^{e_k})
φ(n)=φ(p1e1p2e2…pkek)=φ(p1e1)φ(p2e2)…φ(pkek)
再根据 φ(p^k) = p^k - p^(k-1),我们得到:
φ
(
n
)
=
(
p
1
e
1
−
p
1
e
1
−
1
)
(
p
2
e
2
−
p
2
e
2
−
1
)
…
(
p
k
e
k
−
p
k
e
k
−
1
)
φ(n) = (p_1^{e_1} - p_1^{e_1-1})(p_2^{e_2} - p_2^{e_2-1})…(p_k^{e_k} - p_k^{e_k-1})
φ(n)=(p1e1−p1e1−1)(p2e2−p2e2−1)…(pkek−pkek−1)
进一步化简,得到:
φ
(
n
)
=
n
(
p
1
−
1
p
1
)
(
p
2
−
1
p
2
)
…
(
p
k
−
1
p
k
)
φ(n) = n\left(\frac{p_1-1}{p_1}\right)\left(\frac{p_2-1}{p_2}\right)…\left(\frac{p_k-1}{p_k}\right)
φ(n)=n(p1p1−1)(p2p2−1)…(pkpk−1)
这就是欧拉函数的计算公式。
示例
例如,对于 n = 12,其质因数分解为 12 = 2^2 · 3^1。
应用欧拉函数的计算公式,我们有:
φ
(
12
)
=
12
(
2
−
1
2
)
(
3
−
1
3
)
=
12
×
1
2
×
2
3
=
4
φ(12) = 12\left(\frac{2-1}{2}\right)\left(\frac{3-1}{3}\right) = 12 × \frac{1}{2} × \frac{2}{3} = 4
φ(12)=12(22−1)(33−1)=12×21×32=4
小于或等于 12 的正整数中与 12 互质的数有 1, 5, 7, 11,共 4 个。
根据欧拉函数公式:
φ
(
n
)
=
n
(
p
1
−
1
p
1
)
(
p
2
−
1
p
2
)
…
(
p
k
−
1
p
k
)
φ(n) = n\left(\frac{p_1-1}{p_1}\right)\left(\frac{p_2-1}{p_2}\right)…\left(\frac{p_k-1}{p_k}\right)
φ(n)=n(p1p1−1)(p2p2−1)…(pkpk−1)
要求一个数的欧拉函数,只需要求出这个数的所有质因子即可算出。
欧拉函数
#include<iostream>
#include<algorithm>
using namespace std;
int main(){
int n; cin>>n;
while(n--){
int a;cin>>a;
int res=a;
for(int i=2;i<=a/i;i++)
if(a%i==0){
res=res/i*(i-1);
while(a%i==0) a/=i;
}
if(a>1) res=res/a*(a-1);
cout<<res<<endl;
}
return 0;
}
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N=1000010;
int primes[N],cnt,phi[N];
bool st[N];
LL get_eluro(int n){
phi[1]=1;
for(int i=2;i<=n;i++){
if(!st[i]) {
primes[cnt++]=i;
phi[i]=i-1;
}
// 用已筛出的质数筛后续合数
for(int j=0;primes[j]<=n/i;j++){
st[primes[j]*i]=true;
if(i%primes[j]==0){
phi[primes[j]*i]=primes[j]*phi[i];
break;
}
phi[primes[j]*i]=(primes[j]-1)*phi[i];
}
}
LL res=0;
for(int i=1;i<=n;i++) res+=phi[i];
return res;
}
int main(){
int x;cin>>x;
cout<<get_eluro(x)<<endl;
return 0;
}
快速幂
一.快速幂解法 O ( n ∗ l o g b ) O(n*logb) O(n∗logb)
基本思路:
- 预处理出 a 2 0 , a 2 1 , a 2 2 , … , a 2 l o g b a^{2^0}, a^{2^1}, a^{2^2}, \ldots, a^{2^{logb}} a20,a21,a22,…,a2logb这b个数
- 将
a
b
a^b
ab用
a
2
0
,
a
2
1
,
a
2
2
,
…
,
a
2
l
o
g
b
a^{2^0}, a^{2^1}, a^{2^2}, \ldots, a^{2^{logb}}
a20,a21,a22,…,a2logb这b种数来组合, 即组合成
a
b
=
a
2
x
1
×
a
2
x
2
×
…
×
a
2
x
t
=
a
2
x
1
+
2
x
2
+
…
+
2
x
t
a^b = a^{2^{x_1}} \times a^{2^{x_2}} \times \ldots \times a^{2^{x_t}} = a^{2^{x_1}+2^{x_2}+\ldots+2^{x_t}}
ab=a2x1×a2x2×…×a2xt=a2x1+2x2+…+2xt即用二进制表示
为什么b可用 a 2 0 , a 2 1 , a 2 2 , … , a 2 l o g b a^{2^0}, a^{2^1}, a^{2^2}, \ldots, a^{2^{logb}} a20,a21,a22,…,a2logb这b个数来表示?
二进制可以表示所有数, 且用单一用二进制表示时, b单一表示最大可表示为二进制形式的 2 l o g b 2^{logb} 2logb
注意:
- b&1就是判断b的二进制表示中第0位上的数是否为1, 若为1, b&1=true, 反之b&1=false 还不理解? 进传送门
- b&1也可以用来判断奇数和偶数, b&1=true时为奇数, 反之b&1=false时为偶数
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
//二进制可以表示所有数,且用单一用二进制表示时,b单一表示最大可表示为二进制形式的2^logb
//a^k%p
int qmi(int a,int k,int p){
int res=1;
while(k){//对k进行二进制化,从低位到高位
//如果k的二进制表示的第0位为1,则乘上当前的a
if(k&1) res=(LL)res*a%p;
k>>=1; //b右移一位
a=(LL)a*a%p;//更新a,a依次为a^{2^0},a^{2^1},a^{2^2},....,a^{2^logb}
}
return res;
}
int main(){
int n; scanf("%d",&n);
while(n--){
int a,k,p;
scanf("%d%d%d",&a,&k,&p);
printf("%d\n",qmi(a,k,p));
}
return 0;
}
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
//二进制可以表示所有数,且用单一用二进制表示时,b单一表示最大可表示为二进制形式的2^logb
//a^k%p
int qmi(int a,int k,int p){
int res=1;
while(k){//对k进行二进制化,从低位到高位
//如果k的二进制表示的第0位为1,则乘上当前的a
if(k&1) res=(LL)res*a%p;
k>>=1; //b右移一位
a=(LL)a*a%p;//更新a,a依次为a^{2^0},a^{2^1},a^{2^2},....,a^{2^logb}
}
return res;
}
int main(){
int n; scanf("%d",&n);
while(n--){
int a,p;
scanf("%d%d",&a,&p);
int res=qmi(a,p-2,p);
//1. 因为 p 是质数,所以不用写p%a==0,写了反而会错,因为当a=1时,res应该为1,但是由于p%a==0,会输出impossible
//2. 此处还需 mod p 的原因是题目要求输出 0~p-1 的逆元
if(a % p)printf("%d\n",res);
else cout<<"impossible"<<endl;
}
return 0;
}
扩展欧几里得算法
-
扩展欧几里得
用于求解方程 a x + b y = g c d ( a , b ) ax + by = gcd(a, b) ax+by=gcd(a,b) 的解
当 b = 0 b = 0 b=0 时 a x + b y = a ax + by = a ax+by=a 故而 x = 1 , y = 0 x = 1, y = 0 x=1,y=0
当 b ≠ 0 b \neq 0 b=0 时
因为
g c d ( a , b ) = g c d ( b , a % b ) gcd(a, b) = gcd(b, a \% b) gcd(a,b)=gcd(b,a%b)
而
b x ′ + ( a % b ) y ′ = g c d ( b , a % b ) bx' + (a \% b)y' = gcd(b, a \% b) bx′+(a%b)y′=gcd(b,a%b)
b x ′ + ( a − ⌊ a / b ⌋ ∗ b ) y ′ = g c d ( b , a % b ) bx' + (a - \lfloor a/b \rfloor * b)y' = gcd(b, a \% b) bx′+(a−⌊a/b⌋∗b)y′=gcd(b,a%b)
a y ′ + b ( x ′ − ⌊ a / b ⌋ ∗ y ′ ) = g c d ( b , a % b ) = g c d ( a , b ) ay' + b(x' - \lfloor a/b \rfloor * y') = gcd(b, a \% b) = gcd(a, b) ay′+b(x′−⌊a/b⌋∗y′)=gcd(b,a%b)=gcd(a,b)
故而
x = y ′ , y = x ′ − ⌊ a / b ⌋ ∗ y ′ x = y', \quad y = x' - \lfloor a/b \rfloor * y' x=y′,y=x′−⌊a/b⌋∗y′
因此可以采取递归算法 先求出下一层的 x ′ x' x′ 和 y ′ y' y′ 再利用上述公式回代即可 -
对于求解更一般的方程 a x + b y = c ax + by = c ax+by=c
设 d = g c d ( a , b ) d = gcd(a, b) d=gcd(a,b) 则其有解当且仅当 d ∣ c d | c d∣c
求解方法如下:
用扩展欧几里得求出 a x 0 + b y 0 = d ax_0 + by_0 = d ax0+by0=d 的解
则 a ( x 0 ∗ c / d ) + b ( y 0 ∗ c / d ) = c a(x_0 * c/d) + b(y_0 * c/d) = c a(x0∗c/d)+b(y0∗c/d)=c
故而特解为 x ′ = x 0 ∗ c / d , y ′ = y 0 ∗ c / d x' = x_0 * c/d, \quad y' = y_0 * c/d x′=x0∗c/d,y′=y0∗c/d
而通解 = 特解 + 齐次解
而齐次解即为方程 a x + b y = 0 ax + by = 0 ax+by=0 的解
故而通解为 x = x ′ + k ∗ b / d , y = y ′ − k ∗ a / d k ∈ Z x = x' + k * b/d, \quad y = y' - k * a/d \quad k \in \mathbb{Z} x=x′+k∗b/d,y=y′−k∗a/dk∈Z
若令 t = b / d t = b/d t=b/d,则对于 x x x 的最小非负整数解为 ( x ′ % t + t ) % t (x' \% t + t) \% t (x′%t+t)%t -
应用: 求解一次同余方程 a x ≡ b ( m o d m ) ax \equiv b \ (mod m) ax≡b (modm)
则等价于求
a x = m ∗ ( − y ) + b ax = m \ * (-y) + b ax=m ∗(−y)+b
a x + m y = b ax + my = b ax+my=b
有解条件为 g c d ( a , m ) ∣ b gcd(a, m) | b gcd(a,m)∣b,然后用扩展欧几里得求解即可
特别的 当 b = 1 b = 1 b=1 且 a a a 与 m m m 互质时 则所求的 x x x 即为 a a a 的逆元
#include<iostream>
using namespace std;
int exgcd(int a,int b,int &x,int &y){
if(!b){
x=1;y=0;
return a;
}
int d=exgcd(b,a%b,y,x);//先求再减
y-= a / b *x;
return d;
}
int main(){
int n; scanf("%d",&n);
while(n--){
int a,b,x,y;
scanf("%d%d",&a,&b);
exgcd(a,b,x,y);//即使有返回有可能之间引用
printf("%d %d\n",x,y);
}
return 0;
}
求逆元探讨
当n为质数时,可以用快速幂求逆元:
快速幂求逆元
a
/
b
≡
a
∗
x
(
m
o
d
n
)
a / b \equiv a * x (\bmod n)
a/b≡a∗x(modn)
两边同乘b可得 a ≡ a ∗ b ∗ x ( m o d n ) a \equiv a * b * x (\bmod n) a≡a∗b∗x(modn)
即 1 ≡ b ∗ x ( m o d n ) 1 \equiv b * x (\bmod n) 1≡b∗x(modn)
同 b ∗ x ≡ 1 ( m o d n ) b * x \equiv 1 (\bmod n) b∗x≡1(modn)
由费马小定理可知,当n为质数时
b n − 1 ≡ 1 ( m o d n ) b ^ {n - 1} \equiv 1 (\bmod n) bn−1≡1(modn)
拆一个b出来可得 b ∗ b n − 2 ≡ 1 ( m o d n ) b * b ^ {n - 2} \equiv 1 (\bmod n) b∗bn−2≡1(modn)
故当n为质数时,b的乘法逆元 x = b n − 2 x = b ^ {n - 2} x=bn−2
当n不是质数时,可以用扩展欧几里得算法求逆元:
线性同余方程
a有逆元的充要条件是a与p互质,所以 gcd ( a , p ) = 1 \gcd(a, p) = 1 gcd(a,p)=1
假设a的逆元为x,那么有 a ∗ x ≡ 1 ( m o d p ) a * x \equiv 1 (\bmod p) a∗x≡1(modp)
等价: a x + p y = 1 ax + py = 1 ax+py=1
exgcd(a, p, x, y)
#include<iostream>
using namespace std;
typedef long long LL;
int exgcd(int a,int b,int &x,int &y){
if(!b){
x=1;y=0;
return a;
}
int d=exgcd(b,a%b,y,x);//先求再减
y-= a / b *x;
return d;
}
int main(){
int n; scanf("%d",&n);
while(n--){
int a,b,m;
scanf("%d%d%d",&a,&b,&m);
int x,y;
int d=exgcd(a,m,x,y);//即使有返回有可能之间引用
if(b%d) puts("impossible");
else printf("%lld\n",(LL)x *(b/d) % m);
}
return 0;
}

被折叠的 条评论
为什么被折叠?



