AC咕咕鸡--暑期算法训练记录(7.11启动!) 各专题实时补充

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位

笔记: 若想获取某个数字的第 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} 2k1取模,就可以得到 n n n的最末尾 k − 1 k-1 k1个二进制位的十进制结果 c o c_{\mathrm{o}} co
此时,我们让 x = 2 k − 1 − c x=2^{k-1}-c x=2k1c,即可让 n + x = n − c + 2 k − 1 n+x=n-c+2^{k-1} n+x=nc+2k1的二进制第 k k k位变成 1,而 1 ∼ k − 1 1\sim k-1 1k1位都变成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树用于大量字符串的处理查询删除等操作
仔细理解这种结构后会发现
这也是一个典型的空间换时间的数据结构
请添加图片描述

请添加图片描述

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;
}

在这里插入图片描述
在这里插入图片描述

堆排序的相关概念补充拓展
  1. 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序, 它的最坏、最好、平均时间复杂度均为 O(nlogn), 它也是不稳定排序。
  2. 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值, 这种情况称为大顶堆,注意:没有要求结点的左孩子的值和右孩子的值的大小关系。
  3. 每个结点的值都小于或等于其左右孩子结点的值, 这种情况称为小顶堆。
  4. 大顶堆举例说明:在这里插入图片描述
  5. 小顶堆举例说明:请添加图片描述
  6. 如果要求排序为升序,一般采用大顶堆,每次将堆顶元素与未排序的最后一个元素交换,再调整堆。
  7. 如果要求排序为降序,一般采用小顶堆,每次将堆顶元素与未排序的最后一个元素交换,再调整堆。
数组模拟堆的操作例题

模拟堆(巧妙的映射关系
相关题解

#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 哈希表

对于哈希表的存储方式 我们通常我们使用两种存储方式

  1. 拉链法
  2. 开放寻址法

哈希表模拟
具体比较好的详解:该题详解

//拉链法 (了解模拟单链表的操作原理
#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×Pn1+X2×Pn2++Xn1×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,n1] 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[l1]×Prl+1
注意点:

  1. 任意字符不可以映射成0,否则会出现不同的字符串都映射成0的情况,比如A,AA,AAA皆为0
  2. 冲突问题:通过巧妙设置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=1kpiai=p1a1p2a2pkaki=1k(ai+1)=(a1+1)(a2+1)(ak+1)i=1k(j=0aipij)=i=1k(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 互质的数的个数。下面我们来推导欧拉函数的性质及其计算公式。

欧拉函数的性质

  1. 若 n 是质数 p,则 φ§ = p - 1。
    这是因为除了 1 以外,其他所有小于 p 的正整数都与 p 互质。
  2. 若 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) 个。
  3. 欧拉函数是积性函数:若 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)=φ(p1e1p2e2pkek)=φ(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)=(p1e1p1e11)(p2e2p2e21)(pkekpkek1)
进一步化简,得到:
φ ( 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(p1p11)(p2p21)(pkpk1)
这就是欧拉函数的计算公式。

示例

例如,对于 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(221)(331)=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(p1p11)(p2p21)(pkpk1)
要求一个数的欧拉函数,只需要求出这个数的所有质因子即可算出。
欧拉函数

#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(nlogb)

基本思路:

  1. 预处理出 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个数
  2. 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;
}

扩展欧几里得算法

扩展欧几里得算法

  1. 扩展欧几里得
    用于求解方程 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+(aa/bb)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(xa/by)=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=xa/by
    因此可以采取递归算法 先求出下一层的 x ′ x' x y ′ y' y 再利用上述公式回代即可

  2. 对于求解更一般的方程 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 dc
    求解方法如下:
    用扩展欧几里得求出 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(x0c/d)+b(y0c/d)=c
    故而特解为 x ′ = x 0 ∗ c / d , y ′ = y 0 ∗ c / d x' = x_0 * c/d, \quad y' = y_0 * c/d x=x0c/d,y=y0c/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+kb/d,y=yka/dkZ
    若令 t = b / d t = b/d t=b/d,则对于 x x x 的最小非负整数解为 ( x ′ % t + t ) % t (x' \% t + t) \% t (x%t+t)%t

  3. 应用: 求解一次同余方程 a x ≡ b   ( m o d m ) ax \equiv b \ (mod m) axb (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/bax(modn)

两边同乘b可得 a ≡ a ∗ b ∗ x (   m o d   n ) a \equiv a * b * x (\bmod n) aabx(modn)

1 ≡ b ∗ x (   m o d   n ) 1 \equiv b * x (\bmod n) 1bx(modn)

b ∗ x ≡ 1 (   m o d   n ) b * x \equiv 1 (\bmod n) bx1(modn)

由费马小定理可知,当n为质数时

b n − 1 ≡ 1 (   m o d   n ) b ^ {n - 1} \equiv 1 (\bmod n) bn11(modn)

拆一个b出来可得 b ∗ b n − 2 ≡ 1 (   m o d   n ) b * b ^ {n - 2} \equiv 1 (\bmod n) bbn21(modn)

故当n为质数时,b的乘法逆元 x = b n − 2 x = b ^ {n - 2} x=bn2

当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) ax1(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;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值