ACWing算法基础课

文章目录

  • 1.基础算法
      • 快速排序 O ( n log ⁡ n ) O(n \log n) O(nlogn)
      • 归并排序 O ( n log ⁡ n ) O(n\log n) O(nlogn)
      • 二分算法 O ( log ⁡ n ) O(\log n) O(logn)
        • 整数二分算法
        • 浮点数二分算法
      • 高精度 O ( n ) O(n) O(n)
        • 加法
        • 减法
        • 乘法
        • 除法
      • 前缀和 O ( n ) O(n) O(n)初始化 O ( 1 ) O(1) O(1)查询前缀和
        • 一维前缀和
        • 二位前缀和
      • 差分 O ( n ) O(n) O(n)
        • 一维差分
        • 二维差分
      • 位运算
      • 双指针 O ( n ) O(n) O(n)
      • 离散化 O ( log ⁡ n ) O(\log n) O(logn)
      • 区间合并
  • 2.数据结构
      • C++STL
      • 双链表 O ( n ) O(n) O(n)
      • 单调栈 O ( n ) O(n) O(n)
      • 单调队列 O ( n ) O(n) O(n)
      • KMP O ( n ) O(n) O(n)
      • 字符串哈希
      • 并查集 O ( n log ⁡ n ) O(n \log n) O(nlogn)
      • Trie树 O ( n ) O(n) O(n)
    • 并查集
      • 优化方式
      • 带权并查集
      • 扩展域并查集(枚举)
    • 树状数组
      • 基本原理
        • 树状数组查询 O ( log ⁡ n ) O(\log n) O(logn)
        • 树状数组修改 O ( log ⁡ n ) O(\log n) O(logn)
        • lowbit(i)求法
      • 扩展
        • 区间修改与单点询问
        • 区间修改与区间求和
        • 二维树状数组
    • 线段树
      • node结构体
      • pushup
      • pushdown
      • 建树
      • 询问
      • 单点修改
      • 区间修改
  • 3.搜索与图论
    • 存图
      • 链式前向星
    • 搜索
      • DFS O ( m + n ) O(m+n) O(m+n)
        • 树的重心
      • 宽度优先搜索(BFS) O ( m + n ) O(m+n) O(m+n)
      • 拓扑排序 O ( m + n ) O(m+n) O(m+n)
    • 最短路问题
      • Dijkstra
          • 朴素Dijstra算法 O ( n 2 + m ) O(n^2+m) O(n2+m)
          • 堆优化的算法 O ( m l o g n ) O(mlogn) O(mlogn)
      • Floyd O ( n 3 ) O(n^3) O(n3)
      • Bellman_Ford算法
        • 朴素算法 O ( m n ) O(mn) O(mn)
        • SPFA算法 O ( m ) O(m) O(m)~ O ( m n ) O(mn) O(mn)
        • SPFA求负环 O ( m n ) O(mn) O(mn)
    • 最小生成树
        • Prim
          • 朴素Prim O ( n 2 + m ) O(n^2+m) O(n2+m)
        • Kruskal O ( m l o g m ) O(mlogm) O(mlogm)
    • 二分图
      • 染色法辨别二分图 O ( m + n ) O(m+n) O(m+n)
      • 匈牙利算法 O ( m n ) O(mn) O(mn)
  • 4.数学知识
    • 约数与因数
      • 试除法判定质数
      • 试除法分解质因数 O ( n 1 2 ) O(n^{\frac{1}{2}}) O(n21)
      • 线性筛求素数
      • 试除法求约数
      • 约数个数和约数之和
      • 最大公约数
      • 欧拉函数
      • 线性筛法求欧拉函数原理
      • 欧拉定理
    • 快速幂与其应用
      • 快速幂
      • 乘法逆元
      • 扩展欧几里得算法
        • 裴蜀定理
      • 高斯消元
      • 组合数
        • 递归求组合数
        • 快速幂求组合数
        • Lucas定理求组合数
        • 分解质因数法求组合数
      • 卡特兰数
      • 容斥原理(二进制数表示集合)
    • 博弈论
      • NIM博弈
      • 公平组合游戏ICG
      • 有向图游戏
      • Mex运算
      • SG函数
      • 有向图游戏的和
      • 定理
  • 5. 动态规划
    • 背包问题
      • 0-1背包
      • 完全背包
      • 多重背包
        • 采用二进制优化方式 O ( N V log ⁡ S ) O(NV \log S) O(NVlogS)
      • 分组背包
    • 线性DP
    • 区间DP
    • 计数DP
    • 数位DP
    • 状压DP
    • 树形DP
    • 记忆化搜索
  • 6. 贪心
    • 区间问题
    • Huffman树
    • 排序不等式
    • 绝对值不等式
    • 推公式
  • 7.时空复杂度分析
    • Huffman树
    • 排序不等式
    • 绝对值不等式
    • 推公式
  • 7.时空复杂度分析

1.基础算法

快速排序 O ( n log ⁡ n ) O(n \log n) O(nlogn)

void quick_sort(int a[], int l, int r)
{
    if(l >= r) return;
    
    int x = a[(l+r)>>1], i = l-1, j = r+1;
    while(i < j) {
        do i++; while(a[i] < x);
        do j--; while(a[j] > x);
        if(i < j) swap(a[i],a[j]);
    }
    quick_sort(a,l,j);
    quick_sort(a,j+1,r);
}

归并排序 O ( n log ⁡ n ) O(n\log n) O(nlogn)

void merge_sort(int a[], int l, int r)
{
    if(l >= r) return;

    int mid = l+r>>1;
    merge_sort(a,l,mid);
    merge_sort(a,mid+1,r);

    int k = 0, i = l, j = mid+1;
    while(i <= mid && j <= r) {
        if(a[i] <= a[j]) tmp[k++] = a[i++];
        else tmp[k++] = a[j++];
    }

    while(i <= mid) tmp[k++] = a[i++];
    while(j <= r) tmp[k++] = a[j++];

    for(int x = 0, y = l;y <= r;x++, y++) a[y] = tmp[x];

}

二分算法 O ( log ⁡ n ) O(\log n) O(logn)

整数二分算法

bool check(int x)	// 检查x是否满足某种性质
{
    
}

// 区间[l,r]被划分成[l,mid]和[mid+1,r]时使用

int binary_search(int l, int r)
{
    while(l < r)
    {
        int mid = (l+r)>>1;
        if(check(mid)) r = mid;
        else l = mid+1;
    }
    return l;
}

浮点数二分算法

const double eps = 1e-6;

bool check(double x) // 检查x是否满足某种性质
{
    
}

double binary_search(double l, double r)
{
    while(r-l > eps)
    {
        double mid = (l+r)>>1;
        if(check(mid)) r = mid;
        else l = mid;
    }
    return l;
}

高精度 O ( n ) O(n) O(n)

加法

// C = A + B, A >= 0, B >= 0
vector add(vector &A, vector &B)
{
    if (A.size() < B.size()) return add(B, A);

    vector C;
    int t = 0;
    for (int i = 0; i < A.size(); i ++ )
    {
        t += A[i];
        if (i < B.size()) t += B[i];
        C.push_back(t % 10);
        t /= 10;
    }

    if (t) C.push_back(t);
    return C;
}

减法

// C = A - B, 满足A >= B, A >= 0, B >= 0
vector sub(vector &A, vector &B)
{
    vector C;
    for (int i = 0, t = 0; i < A.size(); i ++ )
    {
        t = A[i] - t;
        if (i < B.size()) t -= B[i];
        C.push_back((t + 10) % 10);
        if (t < 0) t = 1;
        else t = 0;
    }

    while (C.size() > 1 && C.back() == 0) C.pop_back();
    return C;
}

乘法

// C = A * b, A >= 0, b >= 0
vector mul(vector &A, int b)
{
    vector C;

    int t = 0;
    for (int i = 0; i < A.size() || t; i ++ )
    {
        if (i < A.size()) t += A[i] * b;
        C.push_back(t % 10);
        t /= 10;
    }

    while (C.size() > 1 && C.back() == 0) C.pop_back();

    return C;
}

除法

// A / b = C ... r, A >= 0, b > 0
vector div(vector &A, int b, int &r)
{
    vector C;
    r = 0;
    for (int i = A.size() - 1; i >= 0; i -- )
    {
        r = r * 10 + A[i];
        C.push_back(r / b);
        r %= b;
    }
    reverse(C.begin(), C.end());
    while (C.size() > 1 && C.back() == 0) C.pop_back();
    return C;
}

前缀和 O ( n ) O(n) O(n)初始化 O ( 1 ) O(1) O(1)查询前缀和

一维前缀和

S[i] = a[1] + a[2] + ... a[i]
a[l] + ... + a[r] = S[r] - S[l - 1]

二位前缀和

S[i, j] = 第i行j列格子左上部分所有元素的和
//以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]

差分 O ( n ) O(n) O(n)

一维差分

给区间[l, r]中的每个数加上c:B[l] += c, B[r + 1] -= c

二维差分

给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c

位运算

求n的第k位数字: n >> k & 1
返回n的最后一位1:lowbit(n) = n & -n

双指针 O ( n ) O(n) O(n)

for (int i = 0, j = 0; i < n; i ++ )
{
    while (j < i && check(i, j)) j ++ ;

    // 具体问题的逻辑
}
常见问题分类:
    (1) 对于一个序列,用两个指针维护一段区间
    (2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作

799. 最长连续不重复子序列 - AcWing题库

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LMzMJkaE-1629167397783)(C:/Users/DELL/AppData/Roaming/Typora/typora-user-images/image-20210628145907363.png)]

#include
using namespace std;
const int MAXN = 1e5+50;
int a[MAXN], cnt[MAXN], dis[MAXN];

int main()
{
    int n;
    scanf("%d",&n);
    for(int i = 1;i <= n;i++) scanf("%d",&a[i]);
    int ans = 1;
    for(int i = 1,j = 1;j <= n;j++)
    {
        cnt[a[j]]++;
        if(cnt[a[j]] > 1) {
            for(int k = i;k <= dis[a[j]];k++) {
                cnt[a[k]]--;
            }
            i = dis[a[j]]+1;
        }
        dis[a[j]] = j;
        ans = max(ans,j-i+1);
    }
    printf("%d\n",ans);
    return 0;
}

AcWing 800. 数组元素的目标和 - AcWing

#include
using namespace std;
const int N=100010;
int a[N],b[N];
int main()
{
    int n,m,x;
    cin>>n>>m>>x;
    for(int i=0;i>a[i];
    for(int i=0;i>b[i];
    for(int i = 0, j = m-1;i < n;i++)
    {
        while(j >= 0 && a[i] + b[j] > x) j--;
        if(a[i] + b[j] == x) {
            printf("%d %d\n",i,j);
            return 0;
        }
    }
    return 0;
}

离散化 O ( log ⁡ n ) O(\log n) O(logn)

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

802. 区间和 - AcWing题库

#include
using namespace std;

typedef pair PII;
const int MAXN = 3e5+50;
int n, m;
int a[MAXN], sum[MAXN];

vectorall;
vectoradd, query;

int find(int x)
{
    int l = 0, r = all.size()-1;

    while(l < r)
    {
        int mid = (l + r) >> 1;
        if(all[mid] >= x) r = mid;
        else l = mid+1;
    }
    return r+1;
}

vector::iterator unique(vector &a)
{
    int j = 0;
    for(int i = 0;i < a.size();i++)
    {
        if(!i && a[i] != a[i-1])
        {
            a[j++] = a[i];
        }
    }
    return a.begin()+j;
}

int main()
{
    scanf("%d%d",&n,&m);
    while(n--)
    {
        int x, c;
        scanf("%d%d",&x,&c);
        add.push_back({x,c});
        all.push_back(x);
    }

    while(m--)
    {
        int l, r;
        scanf("%d%d",&l,&r);
        query.push_back({l,r});
        all.push_back(l);
        all.push_back(r);
    }

    sort(all.begin(),all.end());
    all.erase(unique(all.begin(),all.end()),all.end());

    for(auto it : add)
    {
        int u = find(it.first);
        a[u] += it.second;
    }

    for(int i = 1;i <= all.size();i++)
    {
        sum[i] = sum[i-1]+a[i];
    }

    for(auto it : query)
    {
        int l = find(it.first);
        int r = find(it.second);
        int res = sum[r]-sum[l-1];
        printf("%d\n",res);
    }

    return 0;
}

#include
using namespace std;
typedef pair PII;
const int MAXN = 3e5 + 50;
int a[MAXN], sum[MAXN];

vectorall;
vectoradd, query;
int x, c;

int main()
{
    int n, m;
    scanf("%d%d",&n,&m);
    while(n--)
    {
        scanf("%d%d",&x,&c);
        add.push_back({x,c});
        all.push_back(x);
    }

    while(m--)
    {
        int l, r;
        scanf("%d%d",&l,&r);
        query.push_back({l,r});
        all.push_back(l);
        all.push_back(r);
    }

    sort(all.begin(),all.end());
    all.erase(unique(all.begin(),all.end()),all.end());

    for(auto it : add)
    {
        int u = lower_bound(all.begin(),all.end(),it.first)-all.begin();
        a[u] += it.second;
    }

    for(int i = 1;i <= all.size();i++)
    {
        sum[i] = sum[i-1] + a[i];
    }

    for(auto it : query)
    {
        int l = lower_bound(all.begin(),all.end(),it.first)-all.begin();
        int r = lower_bound(all.begin(),all.end(),it.second)-all.begin();
        int ans = sum[r]-sum[l-1];
        printf("%d\n",ans);
    }

}

区间合并

// 将所有存在交集的区间合并
void merge(vector &segs)
{
    vector 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
using namespace std;

const int MAXN = 1e5+40;

struct node{
    int l, r;
    friend bool operator<(node &a, node b)
    {
        return a.l < b.l;
    }
};

vectorN;

int main()
{
    int n;
    scanf("%d",&n);
    int re = n;
    while(n--)
    {
        int l, r;
        scanf("%d%d",&l,&r);
        N.push_back({l,r});
    }
    sort(N.begin(),N.end());

    int cnt = 1;
    int ir = N[0].r;
    for(int i = 1;i < re;i++)
    {
        if(N[i].l > ir) {
            cnt++;
            ir = N[i].r;
        }
        else ir = max(ir,N[i].r);
    }
    printf("%d\n",cnt);
    return 0;
}

2.数据结构

C++STL

vector, 变长数组,倍增的思想
    size()  返回元素个数
    empty()  返回是否为空
    clear()  清空
    front()/back()
    push_back()/pop_back()
    begin()/end()
    []
    支持比较运算,按字典序

pair
    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, greater> 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位取反

双链表 O ( n ) O(n) O(n)

// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;

// 初始化
void init()
{
    //0是左端点,1是右端点
    r[0] = 1, l[1] = 0;
    idx = 2;
}

// 在节点a的右边插入一个数x
void insert(int a, int x)
{
    e[idx] = x;
    l[idx] = a, r[idx] = r[a];
    l[r[a]] = idx, r[a] = idx ++ ;
}

// 删除节点a
void remove(int a)
{
    l[r[a]] = l[a];
    r[l[a]] = r[a];
}

单调栈 O ( n ) O(n) O(n)

保证栈顶元素是栈中最大/最小的。

常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
    while (tt && check(stk[tt], i)) tt -- ;
    stk[ ++ tt] = i;
}

830. 单调栈 - AcWing题库

#include
using namespace std;

const int MAXN = 1e5+50;
int stk[MAXN], a[MAXN];

int main()
{
    int n;
    scanf("%d",&n);
    for(int i = 0;i < n;i++) scanf("%d",&a[i]);
    int tt = 0;
    for(int i = 0;i < n;i++)
    {
        while(tt && a[i] <= stk[tt-1])
        {
            tt--;
        }
        if(tt == 0) printf("-1%c", n == 0 ? '\n' : ' ');
        else printf("%d%c",stk[tt-1],n == 0 ? '\n' : ' ');
        stk[tt++] = a[i];
    }
    //printf("\n");
    return 0;
}

单调队列 O ( n ) O(n) O(n)

常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
    while (hh <= tt && check_out(q[hh])) hh ++ ;  // 判断队头,hh++
    while (hh <= tt && check(q[tt], i)) tt -- ;  // 判断队尾是否单调,tt--
    q[ ++ tt] = i;
}

154. 滑动窗口 - AcWing题库

#include
using namespace std;
const int MAXN = 1e6+50;
int a[MAXN];
dequeq1,q2;

int main()
{
    int n, k;
    scanf("%d%d",&n,&k);
    for(int i = 0;i < n;i++) scanf("%d",&a[i]);

    for(int i = 0;i < k;i++)
    {
        while(!q1.empty() && a[i] <= a[q1.back()]) q1.pop_back();
        q1.push_back(i);
    }
    printf("%d ",a[q1.front()]);

    for(int i = k;i < n;i++)
    {
        while(!q1.empty() && a[i] <= a[q1.back()]) q1.pop_back();
        while(!q1.empty() && i - q1.front() + 1 > k) q1.pop_front();
        q1.push_back(i);
        printf("%d ",a[q1.front()]);
    }

    printf("\n");

    for(int i = 0; i < k;i++)
    {
        while(!q2.empty() && a[i] >= a[q2.back()]) q2.pop_back();
        q2.push_back(i);
    }
    printf("%d ",a[q2.front()]);
    for(int i = k;i < n;i++)
    {
        while(!q2.empty() && a[i] >= a[q2.back()]) q2.pop_back();
        while(!q2.empty() && i - q2.front() + 1 > k) q2.pop_front();
        q2.push_back(i);
        printf("%d ",a[q2.front()]);
    }
    printf("\n");

    return 0;
}

手写单调队列

#include
#include
#include
#include

using namespace std;
const int MAXN = 1e6+50;
int n, k;
int a[MAXN], q[MAXN];

int main()
{
    scanf("%d%d",&n,&k);
    for(int i = 1;i <= n;i++) scanf("%d",&a[i]);
    int hh = 0, tt = -1; // hh是头, tt是尾
    for(int i = 1;i <= n;i++)
    {
        while(hh <= tt && i - q[hh] + 1 > k) hh++;
        while(hh <= tt && a[i] <= a[q[tt]]) tt--;
        q[++tt] = i;
        if(i >= k) printf("%d ",a[q[hh]]);
    }
    printf("\n");
    hh = 0, tt = -1;
    for(int i = 1;i <= n;i++)
    {
        while(hh <= tt && i - q[hh] + 1 > k) hh++;
        while(hh <= tt && a[i] >= a[q[tt]]) tt--;
        q[++tt] = i;
        if(i >= k) printf("%d ",a[q[hh]]);
    }
    printf("\n");
    return 0;
}

KMP O ( n ) O(n) O(n)

image-20210630103732732
// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
求模式串的Next数组:
for (int i = 2, j = 0; i <= m; 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 <= n; i ++ )
{
    while (j && s[i] != p[j + 1]) j = ne[j];
    if (s[i] == p[j + 1]) j ++ ;
    if (j == m)
    {
        j = ne[j];
        // 匹配成功后的逻辑
    }
}

831. KMP字符串 - AcWing题库

#include
using namespace std;
const int MAXN = 1e6+50;
int ne[MAXN];
char p[MAXN], s[MAXN];
int n, m;

int main()
{
    cin >> n;
    cin >> p+1;
    cin >> m;
    cin >> s+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)
        {
            j = ne[j];
            printf("%d ",i-n);
        }
    }
    printf("\n");

    return 0;
}

字符串哈希

核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低
小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果

typedef unsigned long long ULL;
ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64

// 初始化
void init()
{
	p[0] = 1;
    for (int i = 1; i <= n; i ++ )
    {
        h[i] = h[i - 1] * P + str[i];
        p[i] = p[i - 1] * P;
    }
}

// 计算子串 str[l ~ r] 的哈希值
ULL get(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];
}

841. 字符串哈希 - AcWing题库

#include
using namespace std;
const int MAXN = 1e5+50;
const int P = 131;
char str[MAXN];

int n, m;
typedef unsigned long long ULL;
ULL h[MAXN], p[MAXN]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64

// 初始化
void init()
{
	p[0] = 1;
    for (int i = 1; i <= n; i ++ )
    {
        h[i] = h[i - 1] * P + str[i];
        p[i] = p[i - 1] * P;
    }
}

// 计算子串 str[l ~ r] 的哈希值
ULL get(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];
}

int main()
{
    scanf("%d%d",&n,&m);
    scanf("%s",str+1);
    init();
    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;
}

并查集 O ( n log ⁡ n ) O(n \log n) O(nlogn)

(1)朴素并查集:

    int p[N]; //存储每个点的祖宗节点

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ ) p[i] = i;

    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);


(2)维护size的并查集:

    int p[N], size[N];
    //p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        size[i] = 1;
    }

    // 合并a和b所在的两个集合:
    size[find(b)] += size[find(a)];
    p[find(a)] = find(b);

837. 连通块中点的数量 - AcWing题库

#include
using namespace std;
const int MAXN = 1e5+50;
int fa[MAXN], sz[MAXN];

int find(int x)
{
    if(x != fa[x]) fa[x] = find(fa[x]);
    return fa[x];
}

int main()
{
    int n, m;
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++)
    {
        fa[i] = i;
        sz[i] = 1;
    }
    while(m--)
    {
        string op;
        int a, b;
        cin >> op;
        if(op == "Q2")
        {
            scanf("%d",&a);
            a = find(a);
            printf("%d\n",sz[a]);
        }
        else if(op == "Q1")
        {
            scanf("%d%d",&a,&b);
            if(find(a) == find(b)) puts("Yes");
            else puts("No");
        }
        else
        {
            scanf("%d%d",&a,&b);
            a = find(a);
            b = find(b);
            if(a != b)
            {
                fa[a] = b;
                sz[b] += sz[a];
            }
            
        }
    }
    return 0;
}

Trie树 O ( n ) O(n) O(n)

image-20210629212433783
int son[N][26], cnt[N], idx = 0;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点
// cnt[]存储以每个节点结尾的单词数量

// 插入一个字符串
void insert(string str)
{
    int p = 0;
    for(int i = 0;str[i] != '\0';i++)
    {
        int u = str[i] - 'a';
        if(!son[p][u]) son[p][u] = ++ idx;
        p = son[p][u];
    }
    cnt[p]++;
}

// 查询字符串出现的次数
int query(string str)
{
    int p = 0;
    for(int i = 0;str[i] != '\0';i++)
    {
        int u = str[i] - 'a';
        if(!son[p][u]) return 0;
        p = son[p][u];
    }
    return cnt[p];
}

835. Trie字符串统计 - AcWing题库

#include
using namespace std;
const int MAXN = 2e5+50;
char s[MAXN];
int cnt[MAXN], son[MAXN][28];
int idx = 0;


void insert(string str)
{
    int p = 0;
    for(int i = 0;str[i] != '\0';i++)
    {
        int u = str[i] - 'a';
        if(!son[p][u]) son[p][u] = ++ idx;
        p = son[p][u];
    }
    cnt[p]++;
}

int query(string str)
{
    int p = 0;
    for(int i = 0;str[i] != '\0';i++)
    {
        int u = str[i] - 'a';
        if(!son[p][u]) return 0;
        p = son[p][u];
    }
    return cnt[p];
}

int main()
{
    int n;
    scanf("%d",&n);
    while(n--)
    {
        string op;
        cin >> op;
        if(op == "I")
        {
            string s;
            cin >> s;
            insert(s);
        }
        else {
            string s;
            cin >> s;
            printf(
                "%d\n",query(s));
        }
    }
    return 0;
}

143. 最大异或对 - AcWing题库

最大异或和

给定一个序列,求序列中两个元素的异或最大值。

字典树进行优化 O ( n l o g n ) O(nlogn) O(nlogn)

每次往与当前位相反的方向走,保证异或和最大。

#include
#include
#include
#include

using namespace std;
typedef long long ll;
const int MAXN = 1e5+50;
int n, a[MAXN], son[MAXN*30][2], idx;

void insert(int x)
{
    int p = 0;
    for(int i = 30;i >= 0;i--)
    {
        int u = x >> i & 1;
        if(!son[p][u]) son[p][u] = ++idx;
        p = son[p][u];
    }
}

int query(int x)
{
    int p = 0, res = 0;
    for(int i = 30;i >= 0;i--)
    {
        int u = x >> i & 1;
        if(son[p][!u]) {
            p = son[p][!u];
            res += 1 << i;
        }
        else p = son[p][u];
    }
    return res;
}

int main()
{
    scanf("%d",&n);
    for(int i = 1;i <= n;i++)
    {
        scanf("%d",&a[i]);
        insert(a[i]);
    }
    int res = 0;
    for(int i = 1;i <= n;i++) res = max(res,query(a[i]));
    printf("%d\n",res);
    
    return 0;
}

并查集

  • 合并两个集合
  • 查询一个元素的祖宗节点
  • 询问两个元素是否在同一个集合中
// 朴素并查集:

int p[N]; //存储每个点的祖宗节点

// 返回x的祖宗节点 + 路径压缩
int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ ) p[i] = i;

// 合并a和b所在的两个集合:
p[find(a)] = find(b);

优化方式

  • 路径压缩 O ( log ⁡ n ) O(\log n) O(logn)
  • 按秩合并 O ( log ⁡ n ) O(\log n) O(logn)
  • 二者都采用 O ( α ( n ) ) O(\alpha(n)) O(α(n))

带权并查集

  • 记录每个集合的大小 绑定到根结点
  • 每个节点到根结点的距离(相对距离) 绑定到每个元素上
// 维护集合size的并查集:

int p[N], size[N];
//p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量

// 返回x的祖宗节点
int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
    p[i] = i;
    size[i] = 1;
}

// 合并a和b所在的两个集合:
size[find(b)] += size[find(a)];
p[find(a)] = find(b);
// 维护到祖宗节点距离的并查集:

int p[N], d[N];
//p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离

// 返回x的祖宗节点
int find(int x)
{
    if (p[x] != x)
    {
        int root = find(p[x]);
        d[x] += d[p[x]];
        p[x] = root;
    }
    return p[x];
}

// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
    p[i] = i;
    d[i] = 0;
}

// 合并a和b所在的两个集合:
p[find(a)] = find(b);
d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量

扩展域并查集(枚举)

1250. 格子游戏 - AcWing题库

并查集解决的是连通性(无向图联通分量)和传递性(家谱关系)问题,并且可以动态的维护。抛开格子不看,任意一个图中,增加一条边形成环当且仅当这条边连接的两点已经联通,于是可以将点分为若干个集合,每个集合对应图中的一个连通块。

#include
#include
#include
#include

using namespace std;
typedef long long ll;

const int MAXN = 40050;
int n, m, res = 1e9;
int a[MAXN], fa[MAXN];

int transfer(int x, int y)	// 二维转换为一维
{
    return (x-1) * n + y;
}

void init()
{
    for(int i = 1;i <= n*n;i++)
    {
        fa[i] = i;
    }
}

int find(int x)
{
    if(x != fa[x]) fa[x] = find(fa[x]);
    return fa[x];
}

int main()
{
    scanf("%d%d",&n,&m);
    init();
    for(int i = 1;i <= m;i++)
    {
        int x, y;
        char op;
        scanf("%d %d %c",&x,&y,&op);
        int res1, res2;
        
        res1 = transfer(x,y);
        if(op == 'D') res2 = transfer(x+1,y);
        else if(op == 'R') res2 = transfer(x,y+1);
        
        int u = find(res1), v = find(res2);
        if(u == v) {
            res = min(res,i);
        }
        else {
            fa[u] = v;
        }
    }
    if(res != 1e9) printf("%d\n",res);
    else puts("draw");
    return 0;
}

1252. 搭配购买 - AcWing题库

并查集+01背包

#include
#include
#include
#include

using namespace std;
typedef long long ll;
const int MAXN = 10010;
int fa[MAXN], c[MAXN], d[MAXN], dp[MAXN];
int n, m, w, id;

struct node{
    int c, d;
}Node[MAXN];

int find(int x)
{
    if(x != fa[x]) fa[x] = find(fa[x]);
    return fa[x];
}

void init()
{
    for(int i = 1;i <= n;i++) fa[i] = i;
}

int main()
{
    scanf("%d%d%d",&n,&m,&w);
    init();
    for(int i = 1;i <= n;i++)
    {
        scanf("%d%d",&c[i],&d[i]);
    }
    for(int i = 1;i <= m;i++)
    {
        int a, b;
        scanf("%d%d",&a,&b);
        int u = find(a), v = find(b);
        
        if(u != v)
        {
            fa[v] = u;
            c[u] += c[v];
            d[u] += d[v];
        }
    }
    
    for(int i = 1;i <= n;i++) {
        if(i == fa[i])
        {
            Node[id].c = c[i];
            Node[id].d = d[i];
            id++;
        }
    }
    
    for(int i = 0;i < id;i++)
    {
        for(int j = w;j >= Node[i].c;j--)
        {
            dp[j] = max(dp[j],dp[j-Node[i].c]+Node[i].d);
        }
    }
    int ans = dp[w];
    printf("%d\n",ans);
    return 0;
}

237. 程序自动分析 - AcWing题库

并查集+离散化

#include
#include
#include
#include
#include

using namespace std;
typedef long long ll;
const int MAXN = 2e6+50;

int n, T, id, cnt;
int fa[MAXN];

vector>equ, iequ;

int all[MAXN];

void init()
{
    for(int i = 0;i <= cnt;i++) fa[i] = i;
}

int search(int x)
{
    int res = lower_bound(all,all+cnt,x)-all;
    return res;
}

int find(int x)
{
    if(x != fa[x]) fa[x] = find(fa[x]);
    return fa[x];
}

int main()
{
    scanf("%d",&T);
    while(T--)
    {
        equ.clear();
        iequ.clear();
        bool flag = true;
        scanf("%d",&n);
        for(int k = 0;k < n;k++)
        {
            int i, j, e;
            scanf("%d%d%d",&i,&j,&e);
            if(e == 1) equ.push_back(make_pair(i,j));
            else iequ.push_back(make_pair(i,j));
            all[id++] = i;
            all[id++] = j;
        }
        sort(all,all+id);
        cnt = unique(all,all+id)-all;
        init();
        for(auto i : equ)
        {
            int x = search(i.first), y = search(i.second);
            int u = find(x), v = find(y);
            fa[u] = v;
        }
        for(auto i : iequ)
        {
            int x = search(i.first), y = search(i.second);
            int u = find(x), v = find(y);
            if(u == v) {
                flag = false;
                break;
            }
        }
        if(flag) puts("YES");
        else puts("NO");
    }
    return 0;
}

树状数组

基本原理

  • 快速求前缀和 O ( log ⁡ n ) O(\log n) O(logn)
  • 修改某一个数 O ( log ⁡ n ) O(\log n) O(logn)

基于二进制优化

x = 2 i k + 2 i k = 1 + 2 i k − 2 + ⋯ + 2 i 1 log ⁡ x ≥ i k ≥ i k − 1 ≥ i k − 2 ≥ ⋯ ≥ i 1 x = 2^{i_k}+2^{i_{k=1}}+2^{i_{k-2}}+\dots +2^{i_1} \qquad \qquad \log x \ge i_k \ge i_{k-1} \ge i_{k-2} \ge \dots \ge i_1 x=2ik+2ik=1+2ik2++2i1logxikik1ik2i1

( x − 2 i 1 , x ] (x-2^{i_1},x] (x2i1,x]

( x − 2 i 1 − 2 i 2 , x − 2 i 1 ] (x-2^{i_1}-2^{i_2},x-2^{i_1}] (x2i12i2,x2i1]

( x − 2 i 1 − 2 i 2 − 2 i 3 , x − 2 i 1 − 2 i 2 ] (x-2^{i_1}-2^{i_2}-2^{i_3},x-2^{i_1}-2^{i_2}] (x2i12i22i3,x2i12i2]

… \dots

k ( x − 2 i 1 − 2 i 2 − ⋯ − 2 i k = 0 , x − 2 i 1 − 2 i 2 − ⋯ − 2 i k − 1 ] (x-2^{i_1}-2^{i_2}-\dots -2^{i_k} = 0,x-2^{i_1}-2^{i_2}-\dots -2^{i_{k-1}}] (x2i12i22ik=0,x2i12i22ik1]

发现区间 ( L , R ] (L,R] (L,R]的区间长度为R二进制的最后一位1 所对应的次幂 l o w b i t ( R ) lowbit(R) lowbitR

区间为 [ R − l o w b i t ( R ) + 1 , R ] [R-lowbit(R)+1,R] [Rlowbit(R)+1,R]

c [ x ] c[x] c[x]为区间 a [ x − l o w b i t ( x ) + 1 , x ] a[x-lowbit(x)+1,x] a[xlowbit(x)+1,x]所有数的和

c i = a i + a i − 1 + … … + a i − l o w b i t ( i ) + 1 c_i = a_i+a_{i-1}+……+a_{i-lowbit(i)+1} ci=ai+ai1++ailowbit(i)+1

c[1] = a[1]

c[2] = a[2]+a[1]

c[3] = a[3]

c[4] = a[4]+a[3]+a[2]+a[1]

ACWing算法基础课_第1张图片

树状数组查询 O ( log ⁡ n ) O(\log n) O(logn)

eg 查询a[1]+a[2]+……+a[7]

a[7] = c[7] a[6]+a[5] = c[6] a[4]+a[3]+a[2]+a[1] = c[4]

7 = ( 111 ) 2 (111)_2 (111)2 6 = ( 110 ) 2 (110)_2 (110)2 4 = ( 100 ) 2 (100)_2 (100)2 每次减去lowbit

// 查询a[1~x]的和
int sum(int x)
{
    int ret = 0;
    while(x)
    {
        ret += c[x];
        x -= lowbit(x);
    }
    return ret;
}

树状数组修改 O ( log ⁡ n ) O(\log n) O(logn)

x的父亲结点是x+lowbit(x)

每次修改会影响这个节点到根路径下的节点

// a[i] += k
void change(int i, int k)
{
    while(i <= n)
    {
        c[i] += k;
        i += lowbit(i);
    }
}

lowbit(i)求法

inline int lowbit(int x)
{
    return x & (-x);
}
inline int lowbit(int x)
{
    return x & (-x);
}

// 查询a[1~x]的和
int sum(int x)
{
    int ret = 0;
    while(x)
    {
        ret += c[x];
        x -= lowbit(x);
    }
    return ret;
}

// a[i] += k
void change(int i, int k)
{
    while(i <= n)
    {
        c[i] += k;
        i += lowbit(i);
    }
}

扩展

区间修改与单点询问

  • a [ L , R ] a[L,R] a[L,R]中每个元素+c
  • a [ x ] a[x] a[x]是多少

采用差分数组+树状数组

区间修改相当于差分数组中修改两个点

单点查询相当于差分数组求前缀和

区间修改与区间求和

区间修改 : 差分数组 a [ L , R ] + = c a[L,R] += c a[L,R]+=c 等价于 b [ R + 1 ] − = c b [ L ] − = c b[R+1] -= c \qquad b[L] -= c b[R+1]=cb[L]=c

区间求和 : a [ 1 ] + a [ 2 ] + a [ 3 ] + ⋯ + a [ x ] a[1]+a[2]+a[3]+\dots+a[x] a[1]+a[2]+a[3]++a[x] = ∑ i = 1 x a [ i ] = ∑ i = 1 x ∑ j = 1 i b [ j ] \sum_{i=1}^xa[i] = \sum_{i=1}^{x} \sum_{j=1}^i b[j] i=1xa[i]=i=1xj=1ib[j]

image-20210709120631174

考虑补集

a [ 1 ] + a [ 2 ] + a [ 3 ] + ⋯ + a [ x ] = ( x + 1 ) ∗ ( b [ 1 ] + b [ 2 ] + ⋯ + b [ x ] ) − ( 1 ∗ b [ 1 ] + 2 ∗ b [ 2 ] + 3 ∗ b [ 3 ] + ⋯ + x ∗ b [ x ] ) a[1]+a[2]+a[3]+\dots+a[x] = (x+1)*(b[1]+b[2]+\dots+b[x])-(1*b[1]+2*b[2]+3*b[3]+\dots+x*b[x]) a[1]+a[2]+a[3]++a[x]=(x+1)(b[1]+b[2]++b[x])(1b[1]+2b[2]+3b[3]++xb[x])

前面为 b [ i ] b[i] b[i]的前缀和,后面为 i ∗ b [ i ] i*b[i] ib[i]的前缀和

二维树状数组

// a[x][y] += z;
int update(int x, int y, int z)
{
    int i = x;
    while(i <= n)
    {
        int j = y;
        while(j <= m)
        {
            c[i][j] += z;
            j += lowbit(j);
        }
        i += lowbit(i);
    }
}

// a[1][1] + …… + a[1][y] + a[2][1] + …… a[2][n] + …… +a[x][1] + …… + a[x][y]
int sum(int x, int y)
{
    int res = 0, i = x;
    while(i > 0)
    {
        j = y;
        while(j > 0)
        {
            res += c[i][j];
            j -= lowbit(j);
        }
        i -= lowbit(i);
    }
    return res;
}

线段树

一个完全二叉树,每个节点代表一个区间。

区间 [ L , R ] [L,R] [L,R]被分为 [ L , m i d ] , [ m i d + 1 , R ] [L,mid],[mid+1,R] [L,mid],[mid+1,R] m i d = ⌊ L + R 2 ⌋ mid = \lfloor \frac{L+R}{2} \rfloor mid=2L+R

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aHksIY0k-1629167397787)(C:/Users/DELL/AppData/Roaming/Typora/typora-user-images/image-20210322084255533.png)]

线段树 当前结点x

父节点 ⌊ x 2 ⌋ \lfloor \frac{x}{2} \rfloor 2x

左儿子 x < < 1 x << 1 x<<1

右儿子 x < < 1 ∣ 1 x << 1 | 1 x<<11

最大范围 n < < 2 n << 2 n<<2

五个操作

  • pushup
  • pushdown
  • build
  • modify
  • query

node结构体

struct node{
    int l, r;
    ll sum, lazy;	// 区间[l,r]的和
};

int n, m;
int w[MAXN];
node tr[MAXN << 2];

pushup

// 求父节点的和
void pushup(int u)
{
    tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}

pushdown

void pushdown(int u)	// 将懒标记下传
{
    ll lazy = tr[u].lazy;
    if(lazy)
    {
        tr[u << 1].lazy += lazy;
        tr[u << 1 | 1].lazy += lazy;
        tr[u << 1].sum += (tr[u << 1].r - tr[u << 1].l + 1) * lazy;
        tr[u << 1 | 1].sum += (tr[u << 1 | 1].r - tr[u << 1 | 1].l + 1)*lazy;
        tr[u].lazy = 0; 	// 父节点的懒标记重置为0
    }
}

建树

//build(1,1,n) 建立区间1到n的线段树
void build(int u, int l, int r)
{
    if(l == r)
    {
        tr[u] = {l,r,w[r]};
        return;
    }
    else
    {
        tr[u] = {l,r,0};
        int mid = (l+r) >> 1;
        build(u << 1, l, mid);
        build(u << 1 | 1, mid+1, r);
        pushup(u);
    }
}

询问

// 求区间和[l,r]
// 调用(1,l,r)
ll query(int u, int l, int r)
{
    if(l <= tr[u].l && tr[u].r <= r) return tr[u].sum;
    else
    {
        pushdown(u);	// 下放懒标记
        int mid = (tr[u].l + tr[u].r) >> 1;
        ll sum = 0;
        if(l <= mid) sum += query(u << 1, l, r);
        if(r > mid) sum += query(u << 1 | 1, l, r);
        return sum;
    }
}

单点修改

// 调用(1,x,v)
// a[x] += v
void modify(int u, int x, int v)
{
    if(tr[u].l == tr[u].r) tr[u].sum += v;
    else
    {
        int mid = (tr[u].l + tr[u].r) >> 1;
        if(x <= mid) modify(u << 1, x, v);
        else modify(u << 1|1, x, v);
        pushup(u);
    }
}

区间修改

// a[l]~a[r] += d
void change(int u, int l, int r, int d)	// 区间修改
{
    if(l <= tr[u].l && tr[u].r <= r) //完全覆盖父节点,则标记一下懒惰标记即可,先不着急往下传,求和需要用到时再往下传
    {
        tr[u].lazy += d;
        tr[u].sum += (tr[u].r - tr[u].l + 1) * d;
        return;
    }
    else
    {
        pushdown(u);
        int mid = (tr[u].l + tr[u].r) >> 1;
        if(l <= mid) change(u << 1, l, r, d);
        if(r > mid) change(u << 1 | 1, l, r, d);
        pushup(u);
    }
}
struct node{
    int l, r;
    ll sum, lazy;	// 区间[l,r]的和
};

int n, m;
int w[MAXN];
node tr[MAXN << 2];

// 求父节点的和
void pushup(int u)
{
    tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}

void pushdown(int u)	// 将懒标记下传
{
    ll lazy = tr[u].lazy;
    if(lazy)
    {
        tr[u << 1].lazy += lazy;
        tr[u << 1 | 1].lazy += lazy;
        tr[u << 1].sum += (tr[u << 1].r - tr[u << 1].l + 1) * lazy;
        tr[u << 1 | 1].sum += (tr[u << 1 | 1].r - tr[u << 1 | 1].l + 1)*lazy;
        tr[u].lazy = 0; 	// 父节点的懒标记重置为0
    }
}

//build(1,1,n) 建立区间1到n的线段树
void build(int u, int l, int r)
{
    if(l == r)
    {
        tr[u] = {l,r,w[r]};
        return;
    }
    else
    {
        tr[u] = {l,r,0};
        int mid = (l+r) >> 1;
        build(u << 1, l, mid);
        build(u << 1 | 1, mid+1, r);
        pushup(u);
    }
}

// 求区间和[l,r]
// 调用(1,l,r)
ll query(int u, int l, int r)
{
    if(l <= tr[u].l && tr[u].r <= r) return tr[u].sum;
    else
    {
        pushdown(u);	// 下放懒标记
        int mid = (tr[u].l + tr[u].r) >> 1;
        ll sum = 0;
        if(l <= mid) sum += query(u << 1, l, r);
        if(r > mid) sum += query(u << 1 | 1, l, r);
        return sum;
    }
}

// 调用(1,x,v)
// a[x] += v
void modify(int u, int x, int v)
{
    if(tr[u].l == tr[u].r) tr[u].sum += v;
    else
    {
        int mid = (tr[u].l + tr[u].r) >> 1;
        if(x <= mid) modify(u << 1, x, v);
        else modify(u << 1|1, x, v);
        pushup(u);
    }
}

// a[l]~a[r] += d
void change(int u, int l, int r, int d)	// 区间修改
{
    if(l <= tr[u].l && tr[u].r <= r) //完全覆盖父节点,则标记一下懒惰标记即可,先不着急往下传,求和需要用到时再往下传
    {
        tr[u].lazy += d;
        tr[u].sum += (tr[u].r - tr[u].l + 1) * d;
        return;
    }
    else
    {
        pushdown(u);
        int mid = (tr[u].l + tr[u].r) >> 1;
        if(l <= mid) change(u << 1, l, r, d);
        if(r > mid) change(u << 1 | 1, l, r, d);
        pushup(u);
    }
}

3.搜索与图论

存图

链式前向星

这种存图方式的数据结构主要是边集数组,顾名思义,图的边是用数组来存储的。
当然想要完美表示图结构,光有一个边集数组还不够,还要有一个数组存储指向每一个点的第一条边的“指针”。
而每一条边都需要存储接下来一条边的“指针”,这样就能够像类似邻接表一样方便遍历每一个点的所有边了。

#include 
#include 
// 最大顶点数
const int V = 100000;
// 最大边数
const int E = 100000;
// 边结构体的定义
struct Edge {
    int to;         // 表示这条边的另外一个顶点
    int next;       // 指向下一条边的数组下标,值为-1表示没有下一条边
};
// head[i] 表示顶点`i`的第一条边的数组下标,-1表示顶点`i`没有边
int head[V];
Edge edge[E];
// 链式前向星初始化,只需要初始化顶点数组就可以了
memset(head, -1, sizeof(head));
// 增加边的方式
// 新增边 a -> b,该边的数组下标为`id`
inline void addedge(int a, int b, int id)
{
    edge[id].to = b;
    edge[id].next = head[a];    // 新增的边要成为顶点`a`的第一条边,而不是最后一条边
    head[a] = id++;
    return;
}
// 遍历从`a`点出去的所有边
for (int i=head[a]; i!=-1; i=e[i].next) {
    // e[i] 就是你当前遍历的边 a -> e[i].to
}

搜索

DFS O ( m + n ) O(m+n) O(m+n)

int dfs(int u)
{
    vis[u] = true;
    for(int i = head[u];i != -1;i = edge[i].next)
    {
        int p = edge[i].to();
        if(vis[p]) continue;
        
        // 处理结点
    }
}

dfs(1);

树的重心

846. 树的重心 - AcWing题库

#include
using namespace std;
const int MAXN = 2e5 + 50;
int head[MAXN];
bool vis[MAXN];
struct Edge{
    int to, next;
}edge[MAXN];
int n, id, ans;

inline void addedge(int a, int b)
{
    edge[id].to = b;
    edge[id].next = head[a];
    head[a] = id;
    id++;
}

// 以u为根的子树中节点个数
int dfs(int u)
{
    int res = 0, sum = 1;
    vis[u] = true;
    for(int i = head[u];i != -1;i = edge[i].next)	// 遍历树种结点
    {
        int p = edge[i].to;
        if(vis[p]) continue;
        int son = dfs(p);
        res = max(res, son);
        sum += son;
    }
    res = max(res,n-sum);	// 剩余点构成子树的节点个数
    ans = min(ans,res);
    return sum;
}

int main()
{
    scanf("%d",&n);
    memset(vis,false,sizeof(vis));
    memset(head,-1,sizeof(head));
    for(int i = 1;i <= n-1;i++)
    {
        int a, b;
        scanf("%d%d",&a,&b);
        addedge(a,b);
        addedge(b,a);
    }
    ans = n;
    dfs(1);
    printf("%d\n",ans);
    return 0;
}

宽度优先搜索(BFS) O ( m + n ) O(m+n) O(m+n)

queue q;
vis[1] = true; // 表示1号点已经被遍历过
q.push(1);

while (q.size())
{
    int u = q.front();
    q.pop();

    for (int i = head[u]; i != -1; i = edge[i].next)
    {
        int p = edge[i].to;
        if(vis[p]) continue;
        vis[p] = true; // 表示点j已经被遍历过
        q.push(p);
    }
}

847. 图中点的层次 - AcWing题库

#include
using namespace std;
const int MAXN = 2e5+50;
const int inf = 0x3f3f3f3f;
int head[MAXN];
struct Edge{
    int to, next;
}edge[MAXN];
bool vis[MAXN];
int dist[MAXN];
int n, m, id;
queueq;

inline void addedge(int a, int b)
{
    edge[id].to = b;
    edge[id].next = head[a];
    head[a] = id;
    id++;
    return;
}

int main()
{
    memset(head,-1,sizeof(head));
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++) dist[i] = inf;
    while(m--)
    {
        int a, b;
        scanf("%d%d",&a,&b);
        addedge(a,b);
    }
    q.push(1);
    vis[1] = true;
    dist[1] = 0;
    
    while(!q.empty())
    {
        int p = q.front();
        q.pop();
        
        for(int i = head[p];i != -1;i = edge[i].next)
        {
            int u = edge[i].to;
            if(vis[u]) continue;
            vis[u] = true;
            dist[u] = min(dist[u],dist[p]+1);
            q.push(u);
        }
    }
    
    if(dist[n] == inf) printf("-1\n");
    else printf("%d\n",dist[n]);
    
    return 0;
}

拓扑排序 O ( m + n ) O(m+n) O(m+n)

848. 有向图的拓扑序列 - AcWing题库

#include
#include
#include
#include
#include
#include
const int MAXN = 2e5+50;
using namespace std;
int n, m, id;
int head[MAXN];
struct Edge{
    int to, next;
}edge[MAXN];
bool vis[MAXN];
int ans[MAXN];
int d[MAXN]; // 存储入度
queueq;

inline void addedge(int a, int b)
{
    edge[id].to = b;
    edge[id].next = head[a];
    head[a] = id;
    id++;
    return;
}

bool topsort()
{
    int cnt = 0;
    for(int i = 1;i <= n;i++) {
        if(d[i] == 0) {
            q.push(i);
        }
    }
    while(!q.empty())
    {
        int u = q.front();
        q.pop();
        ans[cnt++] = u;

        for(int i = head[u];i != -1;i = edge[i].next)
        {
            int p = edge[i].to;
            d[p]--;
            if(d[p] == 0) q.push(p);
        }
    }
    if(cnt < n) return false;
    else return true;
}

int main()
{
    memset(head,-1,sizeof(head));
    scanf("%d%d",&n,&m);
    while(m--)
    {
        int a, b;
        scanf("%d%d",&a,&b);
        addedge(a,b);
        d[b]++;
    }

    bool flag = topsort();
    if(flag == true) {
        for(int i = 0;i < n;i++) printf("%d ",ans[i]);
    }

    else printf("-1\n");

    return 0;
}

最短路问题

  • Floyd算法是多源最短路算法,复杂度最高( n 3 n^3 n3),通常用在点比较少的起点不固定的问题中。能解决负边(负权)但不能解决负环。
  • Dijkstra算法是单源最短路算法,最常用时间复杂度( n 2 n^2 n2)优化后可以达到( n l o g n nlogn nlogn),不能解决负边问题,稀疏图(点的范围很大但是边不多,边的条数 ∣ E ∣ |E| E远小于 ∣ V ∣ ² |V|² V²)需要耗费比较多的空间。
  • SPFA算法适合稀疏图,可以解决带有负权边,负环的问题,但是在稠密图中效率比Dijkstra要低。

Dijkstra

image-20210701091008200
朴素Dijstra算法 O ( n 2 + m ) O(n^2+m) O(n2+m)

时间复杂度 O ( n 2 + m ) O(n^2+m) O(n2+m)

int dijkstra()
{
    memset(dist,INF,sizeof(dist));
    dist[1] = 0;
    
    for(int i = 1;i <= n-1;i++)
    {
        int t = -1;
        for(int j = 1;j <= n;j++)
        {
            if(!vis[j] && (t == -1 || dist[j] < dist[t]))
            {
                t = j;
            }
        }
        vis[t] = true;
        for(int k = head[t];k != -1;k = edge[k].next)
        {
            int u = edge[k].to;
            if(vis[u]) continue;
            if(dist[u] > dist[t] + edge[k].w)
            {
                dist[u] = dist[t] + edge[k].w;
            }
        }
    }
    if(dist[n] == INF) return -1;
    else return dist[n];
}

849. Dijkstra求最短路 I - AcWing题库

#include
#include
#include
#include
#include
using namespace std;

const int MAXN = 1e5 + 50;
const int INF = 0x3f3f3f3f;

int head[MAXN], dist[MAXN];
bool vis[MAXN];

struct Edge{
  int to, next, w;
}edge[MAXN];

int n, m, id;

inline void addedge(int a, int b, int w)
{
    edge[id].to = b;
    edge[id].next = head[a];
    head[a] = id;
    edge[id].w = w;
    id++;
    return;
}

int dijkstra()
{
    memset(dist,INF,sizeof(dist));
    dist[1] = 0;
    
    for(int i = 1;i <= n-1;i++)	// 一共进行n-1次操作
    {
        int t = -1;
        for(int j = 1;j <= n;j++)	// 找出最小值
        {
            if(!vis[j] && (t == -1 || dist[j] < dist[t]))
            {
                t = j;
            }
        }
        vis[t] = true;
        for(int k = head[t];k != -1;k = edge[k].next)	// 遍历所有以此顶点为起始的边
        {
            int u = edge[k].to;
            if(vis[u]) continue;
            if(dist[u] > dist[t] + edge[k].w)
            {
                dist[u] = dist[t] + edge[k].w;
            }
        }
    }
    if(dist[n] == INF) return -1;
    else return dist[n];
}

int main()
{
    scanf("%d%d",&n,&m);
    memset(head,-1,sizeof(head));
    while(m--)
    {
        int a, b, w;
        scanf("%d%d%d",&a,&b,&w);
        addedge(a,b,w);
    }
    int ans = dijkstra();
    printf("%d\n",ans);
}
堆优化的算法 O ( m l o g n ) O(mlogn) O(mlogn)

时间复杂度 O ( m l o g n ) O(mlogn) O(mlogn), n n n 表示点数, m m m 表示边数

int dijkstra()
{
    for(int i = 1;i <= n;i++) dist[i] = 0x3f3f3f3f;
    dist[1] = 0;
    q.push({0,1});

    while(!q.empty())
    {
        auto u = q.top();
        q.pop();
        
        int dis = u.first;
        int idx = u.second;
        
        if(vis[idx]) continue;
        vis[idx] = true;
        
        for(int i = head[idx]; i != -1;i = edge[i].next)
        {
            int p = edge[i].to;
            if(dist[p] > dis + edge[i].w)
            {
                dist[p] = dis + edge[i].w;
                q.push({dist[p],p});	// 优先队列存取最小的dist
            }
        }
    }
    
    if(dist[n] == 0x3f3f3f3f) return -1;
    else return dist[n];
}

850. Dijkstra求最短路 II - AcWing题库

#include
#include
#include
#include
#include
#include

using namespace std;

typedef pair PII;
const int MAXN = 2e5+50;
int head[MAXN];
struct Edge{
    int to, next, w;
}edge[MAXN];
int dist[MAXN];
priority_queue,greater>q;
int n, m, id;
bool vis[MAXN];

inline void addedge(int a, int b, int w)
{
    edge[id].to = b;
    edge[id].next = head[a];
    edge[id].w = w;
    head[a] = id;
    id++;
    return;
}

int dijkstra()
{
    for(int i = 1;i <= n;i++) dist[i] = 0x3f3f3f3f;
    dist[1] = 0;
    q.push({0,1});

    while(!q.empty())
    {
        auto u = q.top();
        q.pop();
        
        int dis = u.first;
        int idx = u.second;
        
        if(vis[idx]) continue;
        vis[idx] = true;
        
        for(int i = head[idx]; i != -1;i = edge[i].next)
        {
            int p = edge[i].to;
            if(dist[p] > dis + edge[i].w)
            {
                dist[p] = dis + edge[i].w;
                q.push({dist[p],p});
            }
        }
    }
    
    if(dist[n] == 0x3f3f3f3f) return -1;
    else return dist[n];
}

int main()
{
    scanf("%d%d",&n,&m);
    memset(head,-1,sizeof(head));
    while(m--)
    {
        int a, b, w;
        scanf("%d%d%d",&a,&b,&w);
        addedge(a,b,w);
    }
    int ans = dijkstra();
    printf("%d\n",ans);

    return 0;
}

Floyd O ( n 3 ) O(n^3) O(n3)

初始化:
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            if (i == j) d[i][j] = 0;
            else d[i][j] = INF;

// 算法结束后,d[a][b]表示a到b的最短距离
void floyd()
{
    for (int k = 1; k <= n; k ++ )
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++ )
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

854. Floyd求最短路 - AcWing题库

#include
#include
#include
#include
#include

const int INF = 1e9;
using namespace std;
int n, m, k;
int dist[205][205];

int main()
{
    scanf("%d%d%d",&n,&m,&k);
    
    for(int i = 1;i <= n;i++)
    {
        for(int j = 1;j <= n;j++)
        {
            if(i == j) dist[i][j] = 0;
            else dist[i][j] = INF;
        }
    }
    
    while(m--)
    {
        int x, y, z;
        scanf("%d%d%d",&x,&y,&z);
        dist[x][y] = min(dist[x][y],z);
    }
    
    for(int k = 1;k <= n;k++)
    {
        for(int i = 1;i <= n;i++)
        {
            for(int j = 1;j <= n;j++)
            {
                dist[i][j] = min(dist[i][j],dist[i][k]+dist[k][j]);
            }
        }
    }
    
    while(k--)
    {
        int x, y;
        scanf("%d%d",&x,&y);
        if(dist[x][y] >= INF/2) printf("impossible\n");	// 因为存在负边权,所以大于INF/2也合理
        else printf("%d\n",dist[x][y]);
    }
    return 0;
}

Bellman_Ford算法

朴素算法 O ( m n ) O(mn) O(mn)

时间复杂度 O ( m n ) O(mn) O(mn)

有边数限制k

bool bellman_ford()
{
    memset(dist,INF,sizeof(dist));
    dist[1] = 0;
    for(int i = 1;i <= k;i++)
    {
        memcpy(last,dist,sizeof(dist));	// 辅助数组存储上一轮结果,避免一次循环中进行多次改变
        for(int j = 0;j < id;j++)	// 遍历所有边进行更新
        {
            int u = edge[j].from;
            int v = edge[j].to;
            if(dist[v] > last[u] + edge[j].w)
            {
                dist[v] = last[u] + edge[j].w;	// 松弛操作
            }
        }
    }
    // n次操作后满足三角不等式dist[a] <= dist[b] + w
    if(dist[n] >= INF /2) return false;
    else return true;
}

853. 有边数限制的最短路 - AcWing题库

#include
#include
#include
#include
#include

using namespace std;
const int MAXN = 10010;
const int INF = 0x3f3f3f3f;
int head[MAXN], dist[MAXN], last[MAXN];
struct Edge{
  int from, next, to, w;
}edge[MAXN];

int n, m, k, id;

inline void addedge(int a, int b, int w)
{
    edge[id].to = b;
    edge[id].from = a;
    edge[id].next = head[a];
    head[a] = id;
    edge[id].w = w;
    id++;
    return;
}

bool bellman_ford()
{
    memset(dist,INF,sizeof(dist));
    dist[1] = 0;
    for(int i = 1;i <= k;i++)
    {
        memcpy(last,dist,sizeof(dist));
        for(int j = 0;j < id;j++)
        {
            int u = edge[j].from;
            int v = edge[j].to;
            if(dist[v] > last[u] + edge[j].w)
            {
                dist[v] = last[u] + edge[j].w;
            }
        }
    }
    if(dist[n] >= INF /2) return false;
    else return true;
}

int main()
{
    scanf("%d%d%d",&n,&m,&k);
    memset(head,-1,sizeof(head));
    while(m--)
    {
        int a, b, w;
        scanf("%d%d%d",&a,&b,&w);
        addedge(a,b,w);
    }
    bool flag = bellman_ford();
    if(flag) printf("%d\n",dist[n]);
    else printf("impossible\n");
    return 0;
}

SPFA算法 O ( m ) O(m) O(m)~ O ( m n ) O(mn) O(mn)

条件:不存在负环

dist[b] = min(dist[b],dist[a]+w)

只有在dist[a]变小时,dist[b]才会变小

SPFA是基于这个进行优化,使用一个队列存储

int n;      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储每个点到1号点的最短距离
bool st[N];     // 存储每个点是否在队列中

// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    queue q;
    q.push(1);
    st[1] = true;

    while (q.size())
    {
        auto t = q.front();
        q.pop();

        st[t] = false;	// 出队删除

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])     // 如果队列中已存在j,则不需要将j重复插入
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

851. spfa求最短路 - AcWing题库

#include
#include
#include
#include
#include

using namespace std;

const int MAXN = 1e5+50;
const int INF = 0x3f3f3f3f;

int head[MAXN], dist[MAXN];
bool vis[MAXN];
struct Edge{
    int to, next, w;
}edge[MAXN];
int n, m, id;
queueq;

inline void addedge(int a, int b, int w)
{
    edge[id].to = b;
    edge[id].next = head[a];
    head[a] = id;
    edge[id].w = w;
    id++;
    return;
}

int spfa()
{
    memset(dist,INF,sizeof(dist));
    
    vis[1] = true;
    dist[1] = 0;
    q.push(1);
    
    while(!q.empty())
    {
        int u = q.front();
        q.pop();
        
        vis[u] = false;
        
        for(int i = head[u]; i != -1;i = edge[i].next)
        {
            int p = edge[i].to;
            if(dist[p] > dist[u] + edge[i].w)
            {
                dist[p] = dist[u] + edge[i].w;
                if(!vis[p])
                {
                    vis[p] = true;
                    q.push(p);
                }
            }
        }
    }
    
    if(dist[n] == INF) return -1;
    else return dist[n];
}

int main()
{
    scanf("%d%d",&n,&m);
    memset(head,-1,sizeof(head));
    while(m--)
    {
        int x, y, z;
        scanf("%d%d%d",&x,&y,&z);
        addedge(x,y,z);
    }
    int ans = spfa();
    if(ans == -1) printf("impossible\n");
    else printf("%d\n",ans);
    return 0;
}

SPFA求负环 O ( m n ) O(mn) O(mn)

bool spfa()
{
    memset(cnt,0,sizeof(cnt));
    memset(dist,INF,sizeof(dist));
    
    dist[1] = 0;
    vis[1] = true;
    q.push(1);
    
    for(int i = 1;i <= n;i++) {
        vis[i] = true;
        q.push(i);
    }
    
    while(!q.empty())
    {
        int u = q.front();
        q.pop();
        
        vis[u] = false;
        
        for(int i = head[u];i != -1;i = edge[i].next)
        {
            int p = edge[i].to;
            
            if(dist[p] > dist[u] + edge[i].w)
            {
                dist[p] = dist[u] + edge[i].w;
                cnt[p] = cnt[u] + 1;
                if(cnt[p] >= n) return true;
                if(!vis[p])
                {
                    vis[p] = true;
                    q.push(p);
                }
            }
        }
    }
    return false;
}

852. spfa判断负环 - AcWing题库

#include
#include
#include
#include
#include

using namespace std;
const int MAXN = 10010;
const int INF = 0x3f3f3f3f;
int head[MAXN], cnt[MAXN], dist[MAXN];
bool vis[MAXN];
struct Edge{
    int to, next, w;
}edge[MAXN];
queueq;
int n, m, id;

inline void addedge(int a, int b, int w)
{
    edge[id].to = b;
    edge[id].next = head[a];
    head[a] = id;
    edge[id].w = w;
    id++;
    return;
}

bool spfa()
{
    memset(cnt,0,sizeof(cnt));
    memset(dist,INF,sizeof(dist));
    
    dist[1] = 0;
    vis[1] = true;
    q.push(1);
    
    for(int i = 1;i <= n;i++) {
        vis[i] = true;
        q.push(i);
    }
    
    while(!q.empty())
    {
        int u = q.front();
        q.pop();
        
        vis[u] = false;
        
        for(int i = head[u];i != -1;i = edge[i].next)
        {
            int p = edge[i].to;
            
            if(dist[p] > dist[u] + edge[i].w)
            {
                dist[p] = dist[u] + edge[i].w;
                cnt[p] = cnt[u] + 1;
                if(cnt[p] >= n) return true;
                if(!vis[p])
                {
                    vis[p] = true;
                    q.push(p);
                }
            }
        }
    }
    return false;
    
}

int main()
{
    scanf("%d%d",&n,&m);
    memset(head,-1,sizeof(head));
    while(m--)
    {
        int x, y, z;
        scanf("%d%d%d",&x,&y,&z);
        addedge(x,y,z);
    }
    bool flag = spfa();
    if(flag) printf("Yes\n");
    else printf("No\n");
    return 0;
}

最小生成树

Prim

朴素Prim O ( n 2 + m ) O(n^2+m) O(n2+m)
int n;      // n表示点数
int g[N][N];        // 邻接矩阵,存储所有边
int dist[N];        // 存储其他点到当前最小生成树的距离
bool st[N];     // 存储每个点是否已经在生成树中


// 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和
int prim()
{
    memset(dist, 0x3f, sizeof dist);

    int res = 0;
    for (int i = 0; i < n; i ++ )
    {
        int t = -1;
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;

        if (i && dist[t] == INF) return INF;

        if (i) res += dist[t];
        st[t] = true;

        for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);
    }

    return res;
}

858. Prim算法求最小生成树 - AcWing题库

#include
#include
#include
#include
#include

using namespace std;
const int MAXN = 1e5+50;
const int INF = 0x3f3f3f3f;
int dist[MAXN];
bool vis[MAXN];
int w[505][505];
int n, m;

int prim()
{
    int res = 0;
    memset(dist,INF,sizeof(dist));
    for(int i = 0;i < n;i++)
    {
        int t = -1;
        for(int j = 1;j <= n;j++)
        {
            if(!vis[j] && (t == -1 || dist[t] > dist[j]))
            {
                t = j;
            }
        }

        if(i && dist[t] == INF) return INF;

        if(i) res += dist[t];
        vis[t] = true;

        for(int j = 1;j <= n;j++) dist[j] = min(dist[j],w[t][j]);
    }
    return res;
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++)
    {
        for(int j = 1;j <= n;j++)
        {
            if(i == j) w[i][j] = 0;
            else w[i][j] = INF;
        }
    }
    while(m--)
    {
        int x, y, z;
        scanf("%d%d%d",&x,&y,&z);
        w[x][y] = w[y][x] = min(w[x][y],z);
    }
    int ans = prim();
    if(ans == INF) printf("impossible\n");
    else printf("%d\n",ans);
    return 0;
}

Kruskal O ( m l o g m ) O(mlogm) O(mlogm)

#include
#include
#include
#include
#include
using namespace std;
const int MAXN = 2e5+50;

struct Edge{
    int from, to, w;
    bool operator<(const Edge &a)
    {
        return w < a.w;
    }
}edge[MAXN];

int n, m;
int fa[MAXN];

int find(int x)
{
    if(x != fa[x]) fa[x] = find(fa[x]);
    return fa[x];
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i = 0;i < m;i++)
    {
        int x, y, w;
        scanf("%d%d%d",&x,&y,&w);
        edge[i].from = x;
        edge[i].to = y;
        edge[i].w = w;
    }
    
    int res = 0, cnt = 0;
    
    for(int i = 1;i <= n;i++) fa[i] = i;
    sort(edge,edge+m);
    
    for(int i = 0;i < m;i++)
    {
        int from = edge[i].from;
        int to = edge[i].to;
        int u = find(from), v = find(to);
        if(u != v) {
            cnt++;
            res += edge[i].w;
            fa[u] = v;
        }
    }
    
    if(cnt < n-1) printf("impossible\n");
    else printf("%d\n",res);
    
    return 0;
}

二分图

染色法辨别二分图 O ( m + n ) O(m+n) O(m+n)

int n;      // n表示点数
int h[N], e[M], ne[M], idx;     // 邻接表存储图
int color[N];       // 表示每个点的颜色,-1表示未染色,0表示白色,1表示黑色

// 参数:u表示当前节点,c表示当前点的颜色
bool dfs(int u, int c)
{
    color[u] = c;
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (color[j] == -1)
        {
            if (!dfs(j, !c)) return false;
        }
        else if (color[j] == c) return false;
    }

    return true;
}

bool check()
{
    memset(color, -1, sizeof color);
    bool flag = true;
    for (int i = 1; i <= n; i ++ )
        if (color[i] == -1)
            if (!dfs(i, 0))
            {
                flag = false;
                break;
            }
    return flag;
}

匈牙利算法 O ( m n ) O(mn) O(mn)

int n1, n2;     // n1表示第一个集合中的点数,n2表示第二个集合中的点数
int h[N], e[M], ne[M], idx;     // 邻接表存储所有边,匈牙利算法中只会用到从第一个集合指向第二个集合的边,所以这里只用存一个方向的边
int match[N];       // 存储第二个集合中的每个点当前匹配的第一个集合中的点是哪个
bool st[N];     // 表示第二个集合中的每个点是否已经被遍历过

bool find(int x)
{
    for (int i = h[x]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j])
        {
            st[j] = true;
            if (match[j] == 0 || find(match[j]))
            {
                match[j] = x;
                return true;
            }
        }
    }

    return false;
}

// 求最大匹配数,依次枚举第一个集合中的每个点能否匹配第二个集合中的点
int res = 0;
for (int i = 1; i <= n1; i ++ )
{
    memset(st, false, sizeof st);
    if (find(i)) res ++ ;
}

4.数学知识

约数与因数

试除法判定质数

bool is_prime(int x)
{
    if (x < 2) return false;
    for (int i = 2; i <= x / i; i ++ )
        if (x % i == 0)
            return false;
    return true;
}

试除法分解质因数 O ( n 1 2 ) O(n^{\frac{1}{2}}) O(n21)

void get_primes(int n)
{
    for(int i = 2;i <= n/i;i++)
    {
        int s = 0;
        while(n % i == 0)
        {
            n /= i;
            s++;
        }
        if(s) printf("%d %d\n",i,s);
    }
    if(n > 1) printf("%d 1\n",n);
    printf("\n");
}

线性筛求素数

int primes[MAXN], cnt;     // primes[]存储所有素数
bool vis[MAXN];         // vis[x]存储x是否被筛掉

void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (!vis[i]) primes[cnt ++ ] = i;
        for (int j = 0; primes[j] * i <= n ; j ++ )
        {
            vis[primes[j] * i] = true;
            if (i % primes[j] == 0) break;
        }
    }
}

试除法求约数

void get_divisors(int n)
{
    for(int i = 1;i * i <= n;i++)
    {
        if(n % i == 0)
        {
            res.push_back(i);
            if(i * i != n) res.push_back(n/i);
        }
    }
    sort(res.begin(),res.end());
}

约数个数和约数之和

如果 N = p1^c1 * p2^c2 * ... *pk^ck
约数个数: (c1 + 1) * (c2 + 1) * ... * (ck + 1)
约数之和: (p1^0 + p1^1 + ... + p1^c1) * ... * (pk^0 + pk^1 + ... + pk^ck)

最大公约数

int gcd(int a, int b)
{
    return b ? gcd(b, a % b) : a;
}

欧拉函数

φ ( n ) \varphi(n) φ(n)表示 1 − N 1-N 1N中与 N N N互质数的个数。

若在算数基本定理中, N = p 1 α 1 p 2 α 2 … p n α n N=p_1^{\alpha_1}p_2^{\alpha_2}\dots p_n^{\alpha_n} N=p1α1p2α2pnαn

φ ( N ) = N ∗ p 1 − 1 p 1 ∗ p 2 − 1 p 2 ⋯ ∗ p n − 1 p n \varphi(N) = N * \frac {p_1-1}{p_1} * \frac {p_2-1}{p_2} \dots * \frac {p_n-1}{p_n} φ(N)=Np1p11p2p21pnpn1

#include
#include
#include
#include
using namespace std;
int n;

int main()
{
    scanf("%d",&n);
    while(n--)
    {
        int x;
        scanf("%d",&x);
        int res = x;
        for(int i = 2;i * i <= x;i++)
        {
            if(x % i == 0) {
                res = res / i * (i-1);
                while(x % i == 0) x /= i;
            }
        }
        if(x > 1) res = res / x * (x-1);
        printf("%d\n",res);
    }
    return 0;
}

线性筛法求欧拉函数原理

原理

欧拉函数与分解质因数后质数的次数无关

因此 φ ( i ∗ p j ) \varphi(i*p_j) φ(ipj)可以分为两种情况, p j p_j pj为素数

i i i p j p_j pj互质

φ ( i ∗ p j ) = ( i ∗ p j ) ∗ ( 1 − 1 p 1 ) ∗ ⋯ ∗ ( 1 − 1 p j ) … ( 1 − 1 p k ) = φ ( i ) ∗ p j ∗ ( 1 − 1 p j ) = φ ( i ) ∗ ( p j − 1 ) \varphi(i*p_j) = (i*p_j)*(1-\frac{1}{p_1})* \dots * (1-\frac{1}{p_j}) \dots (1-\frac{1}{p_k}) = \varphi(i)*p_j*(1-\frac{1}{p_j}) = \varphi(i) * (p_j-1) φ(ipj)=(ipj)(1p11)(1pj1)(1pk1)=φ(i)pj(1pj1)=φ(i)(pj1)

i i i中有 p j p_j pj这个因子

v a r p h i ( i ∗ p j ) = ( i ∗ p j ) ∗ ( 1 − 1 p 1 ) ∗ ⋯ ∗ ( 1 − 1 p j ) … ( 1 − 1 p k ) = φ ( i ) ∗ p j varphi(i*p_j) = (i*p_j)*(1-\frac{1}{p_1})* \dots * (1-\frac{1}{p_j}) \dots (1-\frac{1}{p_k}) = \varphi(i)*p_j varphi(ipj)=(ipj)(1p11)(1pj1)(1pk1)=φ(i)pj

void get_euler(int n)
{
    euler[1] = 1;

    for(int i = 2;i <= n;i++)
    {
        if(!vis[i])
        {
            prime[cnt++] = i;
            euler[i] = i-1;
        }

        for(int j = 0;prime[j] * i <= n;j++)
        {
            int t = i * prime[j];
            vis[t] = true;
            if(i % prime[j] == 0)
            {
                euler[t] = euler[i] * prime[j];
                break;
            }
            euler[t] = euler[i] * (prime[j]-1);
        }
    }
}

欧拉定理

a a a n n n互质,则有 a φ ( n ) ≡ 1 ( m o d n ) a^{\varphi(n)} \equiv 1 (mod \quad n) aφ(n)1(modn)

证明:

1~n 中, a 1 , a 2 , … , a φ ( n ) a_1,a_2,\dots, a_{\varphi(n)} a1,a2,,aφ(n)为均与 n n n互质的数

同时因为 a a a n n n互质, a a 1 , a a 2 , … , a φ ( n ) aa_1,aa_2,\dots,a_{\varphi(n)} aa1,aa2,,aφ(n) n n n互质

a a i ≡ a a j ≡ 1 m o d    n aa_i \equiv aa_j \equiv 1 \mod n aaiaaj1modn

a ( a i − a j ) ≡ 0 m o d    n a(a_i-a_j) \equiv 0 \mod n a(aiaj)0modn

a i ≡ a j m o d    n a_i \equiv a_j \mod n aiajmodn

所以 a 1 , a 2 , a 3 , … a φ ( n ) a_1,a_2,a_3,\dots a_{\varphi(n)} a1,a2,a3,aφ(n) a a 1 , a a 2 , a a 3 , … , a φ ( n ) aa_1,aa_2,aa_3,\dots, a_{\varphi(n)} aa1,aa2,aa3,,aφ(n)只是 m o d    n \mod n modn 下的一个重排

所以 a φ ( n ) ∗ a 1 ∗ a 2 ⋯ ∗ a v a r p h i ( n ) ≡ a 1 ∗ a 2 ⋯ ∗ a v a r p h i ( n ) m o d    n a^{\varphi(n)}*a_1*a_2\dots *a_{varphi(n)}\equiv a_1*a_2\dots *a_{varphi(n)} \mod n aφ(n)a1a2avarphi(n)a1a2avarphi(n)modn

因此 a φ ( n ) ≡ 1 ( m o d n ) a^{\varphi(n)} \equiv 1 (mod \quad n) aφ(n)1(modn)

快速幂与其应用

快速幂

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

乘法逆元

a b = a ∗ b − 1 m o d    m \frac{a}{b} = a * b^{-1} \mod m ba=ab1modm

b − 1 b^{-1} b1为逆元

存在条件:b与m互质,即m为质数时b存在逆元

即是 b ∗ x ≡ 1 m o d    n b*x \equiv 1 \mod n bx1modn x的解

根据费马小定理 b n − 1 ≡ 1 m o d    n b^{n-1} \equiv 1 \mod n bn11modn

此时 b n − 2 b^{n-2} bn2 b b b的一个乘法逆元

扩展欧几里得算法

裴蜀定理

对于任意正整数a,b。一定存在整数x,y,满足 a x + b y = g c d ( a , b ) ax+by = gcd(a,b) ax+by=gcd(a,b)

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时, g c d ( a , b ) = a gcd(a,b)=a gcd(a,b)=a x = 1 , y = 0 x=1,y=0 x=1,y=0是满足方程的一组解。

g c d ( a , b ) = g c d ( b , a   m o d   b ) gcd(a,b) = gcd(b,a\,mod\,b) gcd(a,b)=gcd(b,amodb)。代入原式中,发现 b y + ( a − ⌊ a b ⌋ ∗ b ) ∗ x = g c d ( b , a   m o d   b ) by+(a-\lfloor \frac{a}{b} \rfloor*b)*x = gcd(b,a\,mod\,b) by+(abab)x=gcd(b,amodb)

所以 b y + a ∗ ( y − ⌊ a b ⌋ ∗ x ) = g c d ( b , a   m o d   b ) by+a*(y-\lfloor \frac{a}{b} \rfloor*x) = gcd(b,a\,mod\,b) by+a(ybax)=gcd(b,amodb)

// 求x, y,使得ax + by = gcd(a, b)
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;
}

高斯消元

// a[N][N]是增广矩阵
int gauss()
{
    int c, r;
    for (c = 0, r = 0; c < n; c ++ )
    {
        int t = r;
        for (int i = r; i < n; i ++ )   // 找到绝对值最大的行
            if (fabs(a[i][c]) > fabs(a[t][c]))
                t = i;

        if (fabs(a[t][c]) < eps) continue;

        for (int i = c; i <= n; i ++ ) swap(a[t][i], a[r][i]);      // 将绝对值最大的行换到最顶端
        for (int i = n; i >= c; i -- ) a[r][i] /= a[r][c];      // 将当前行的首位变成1
        for (int i = r + 1; i < n; i ++ )       // 用当前行将下面所有的列消成0
            if (fabs(a[i][c]) > eps)
                for (int j = n; j >= c; j -- )
                    a[i][j] -= a[r][j] * a[i][c];

        r ++ ;
    }

    if (r < n)
    {
        for (int i = r; i < n; i ++ )
            if (fabs(a[i][n]) > eps)
                return 2; // 无解
        return 1; // 有无穷多组解
    }

    for (int i = n - 1; i >= 0; i -- )
        for (int j = i + 1; j < n; j ++ )
            a[i][n] -= a[i][j] * a[j][n];

    return 0; // 有唯一解
}

组合数

递归求组合数

// c[a][b] 表示从a个苹果中选b个的方案数
for (int i = 0; i < N; i ++ )
    for (int j = 0; j <= i; j ++ )
        if (!j) c[i][j] = 1;
        else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;

快速幂求组合数

//首先预处理出所有阶乘取模的余数fact[N],以及所有阶乘取模的逆元infact[N]
//如果取模的数是质数,可以用费马小定理求逆元
int qmi(int a, int b, int p) // 快速幂
{
    int res = 1;
    while(b)
    {
        if(b & 1) res = (ll)res * a % p;
        b >>= 1;
        a = (ll)a * a % p;
    }
    return res;
}

void init()// 预处理阶乘的余数和阶乘逆元的余数
{
    fact[0] = infact[0] = 1;
    for(int i = 1;i <= 1e5+5;i++)
    {
        fact[i] = (ll)fact[i-1] * i % MOD;
        infact[i] = (ll)infact[i-1] * qmi(i,MOD-2,MOD) % MOD;
    }
}

Lucas定理求组合数

若p是质数,则对于任意整数 1 <= m <= n,有:
C(n, m) = C(n % p, m % p) * C(n / p, m / p) (mod p)
int qmi(int a, int k, int p)  // 快速幂模板
{
    int res = 1 % p;
    while (k)
    {
        if (k & 1) res = (LL)res * a % p;
        a = (LL)a * a % p;
        k >>= 1;
    }
    return res;
}

int C(int a, int b, int p)  // 通过定理求组合数C(a, b)
{
    if (a < b) return 0;

    LL x = 1, y = 1;  // x是分子,y是分母
    for (int i = a, j = 1; j <= b; i --, j ++ )
    {
        x = (LL)x * i % p;
        y = (LL) y * j % p;
    }

    return x * (LL)qmi(y, p - 2, p) % p;
}

int lucas(LL a, LL b, int p)
{
    if (a < p && b < p) return C(a, b, p);
    return (LL)C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;
}

分解质因数法求组合数

当我们需要求出组合数的真实值,而非对某个数的余数时,分解质因数的方式比较好用:
    1. 筛法求出范围内的所有质数
    2. 通过 C(a, b) = a! / b! / (a - b)! 这个公式求出每个质因子的次数。 n! 中p的次数是 n / p + n / p^2 + n / p^3 + ...
    3. 用高精度乘法将所有质因子相乘

int primes[N], cnt;     // 存储所有质数
int sum[N];     // 存储每个质数的次数
bool st[N];     // 存储每个数是否已被筛掉


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


int get(int n, int p)       // 求n!中的次数
{
    int res = 0;
    while (n)
    {
        res += n / p;
        n /= p;
    }
    return res;
}


vector mul(vector a, int b)       // 高精度乘低精度模板
{
    vector c;
    int t = 0;
    for (int i = 0; i < a.size(); i ++ )
    {
        t += a[i] * b;
        c.push_back(t % 10);
        t /= 10;
    }

    while (t)
    {
        c.push_back(t % 10);
        t /= 10;
    }

    return c;
}

get_primes(a);  // 预处理范围内的所有质数

for (int i = 0; i < cnt; i ++ )     // 求每个质因数的次数
{
    int p = primes[i];
    sum[i] = get(a, p) - get(b, p) - get(a - b, p);
}

vector res;
res.push_back(1);

for (int i = 0; i < cnt; i ++ )     // 用高精度乘法将所有质因子相乘
    for (int j = 0; j < sum[i]; j ++ )
        res = mul(res, primes[i]);

卡特兰数

给定n个0和n个1,它们按照某种顺序排成长度为2n的序列,满足任意前缀中0的个数都不少于1的个数的序列的数量为:
Cat(n) = C(2n, n) / (n + 1)

容斥原理(二进制数表示集合)

890. 能被整除的数 - AcWing题库

#include
#include
#include
#include

using namespace std;

typedef long long ll;
const int MAXN = 20;
int n, m;
int primes[MAXN];

int main()
{
    scanf("%d%d",&n,&m);
    for(int i = 0;i < m;i++) scanf("%d",&primes[i]);
    
    ll res = 0;
    
    for(int i = 1;i < 1 << m;i++)
    {
        int t = 1, cnt = 0;
        for(int j = 0;j < m;j++)
        {
            if(i >> j & 1)
            {
                cnt++;
                if((ll)t * primes[j] > n)
                {
                    t = -1;
                    break;
                }
                t *= primes[j];
            }
        }
        if(t != -1)
        {
            if(cnt % 2 == 1) res += n / t;
            else res -= n / t;
        }
    }
    
    printf("%d\n",res);
    
    return 0;
}

博弈论

NIM博弈

给定N堆物品,第i堆物品有 A i A_i Ai个。两名玩家轮流行动,每次可以任选一堆,取走任意多个物品,可把一堆取光,但不能不取。取走最后一件物品者获胜。两人都采取最优策略,问先手是否必胜。

我们把这种游戏称为NIM博弈。把游戏过程中面临的状态称为局面。整局游戏第一个行动的称为先手,第二个行动的称为后手。若在某一局面下无论采取何种行动,都会输掉游戏,则称该局面必败。
所谓采取最优策略是指,若在某一局面下存在某种行动,使得行动后对面面临必败局面,则优先采取该行动。同时,这样的局面被称为必胜。我们讨论的博弈问题一般都只考虑理想情况,即两人均无失误,都采取最优策略行动时游戏的结果。
NIM博弈不存在平局,只有先手必胜和先手必败两种情况。

定理: NIM博弈先手必胜,当且仅当 A 1 A_1 A1 ^$ A_2$ ^ … ^ A n A_n An ≠ \ne = 0

公平组合游戏ICG

若一个游戏满足:

  • 由两名玩家交替行动;
  • 在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;
  • 不能行动的玩家判负;
  • 则称该游戏为一个公平组合游戏。
  • NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件2和条件3。

有向图游戏

给定一个有向无环图,图中有一个唯一的起点,在起点上放有一枚棋子。两名玩家交替地把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动者判负。该游戏被称为有向图游戏。
任何一个公平组合游戏都可以转化为有向图游戏。具体方法是,把每个局面看成图中的一个节点,并且从每个局面向沿着合法行动能够到达的下一个局面连有向边。

Mex运算

设S表示一个非负整数集合。定义mex(S)为求出不属于集合S的最小非负整数的运算,即:
mex(S) = min{x}, x属于自然数,且x不属于S

SG函数

在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点y1, y2, …, yk,定义SG(x)为x的后继节点y1, y2, …, yk 的SG函数值构成的集合再执行mex(S)运算的结果,即:
SG(x) = mex({SG(y1), SG(y2), …, SG(yk)})
特别地,整个有向图游戏G的SG函数值被定义为有向图游戏起点s的SG函数值,即SG(G) = SG(s)。

有向图游戏的和

设G1, G2, …, Gm 是m个有向图游戏。定义有向图游戏G,它的行动规则是任选某个有向图游戏Gi,并在Gi上行动一步。G被称为有向图游戏G1, G2, …, Gm的和。
有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数值的异或和,即:
SG(G) = SG(G1) ^ SG(G2) ^ … ^ SG(Gm)

定理

有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于0。
有向图游戏的某个局面必败,当且仅当该局面对应节点的SG函数值等于0。

893. 集合-Nim游戏 - AcWing题库

#include
#include
#include
#include
#include

using namespace std;

const int N = 110, M = 11010;
int s[N], f[M];
int n, k;

int sg(int x)
{
    if(f[x] != -1) return f[x];
    
    unordered_set S;
    for(int i = 0;i < k;i++)
    {
        if(x >= s[i]) S.insert(sg(x-s[i]));
    }
    for(int i = 0;;i++)
    {
        if(S.count(i) == 0) {
            f[x] = i;
            return f[x];
        }
    }
}

int main()
{
    memset(f,-1,sizeof(f));
    scanf("%d",&k);
    for(int i = 0;i < k;i++) scanf("%d",&s[i]);
    scanf("%d",&n);
    int res = 0;
    for(int i = 0;i < n;i++)
    {
        int x;
        scanf("%d",&x);
        res ^= sg(x);
    }
    if(res) puts("Yes");
    else puts("No");
    return 0;
}

5. 动态规划

背包问题

0-1背包

image-20210705102158933

由于 f [ i ] f[i] f[i]只用到了 f [ i − 1 ] f[i-1] f[i1],且第二位背包容量不超过 j j j,可以从二维变成一维。

#include
#include
#include

using namespace std;
int N, V;
const int MAXN = 1010;
int v[MAXN], w[MAXN], dp[MAXN];

int main()
{
    scanf("%d%d",&N,&V);
    for(int i = 1;i <= N;i++) scanf("%d%d",&v[i],&w[i]);
    for(int i = 1;i <= N;i++)
    {
        for(int j = V;j >= v[i];j--)
        {
            dp[j] = max(dp[j],dp[j-v[i]]+w[i]);
        }
    }
    int ans = dp[V];
    printf("%d\n",ans);
    return 0;
}

完全背包

image-20210705105407769

以上时间复杂度为 O ( n 3 ) O(n^3) O(n3),可以优化为 O ( n 2 ) O(n^2) O(n2)

image-20210705105746552
#include
#include
#include

using namespace std;
const int MAXN = 1010;

int v[MAXN], w[MAXN], dp[MAXN];
int N, V;

int main()
{
    scanf("%d%d",&N,&V);
    for(int i = 1;i <= N;i++) scanf("%d%d",&v[i],&w[i]);
    
    for(int i = 1;i <= N;i++)
    {
        for(int j = v[i];j <= V;j++)
        {
            dp[j] = max(dp[j],dp[j-v[i]]+w[i]);
        }
    }
    
    printf("%d\n",dp[V]);
    
    return 0;
}

多重背包

ACWing算法基础课_第2张图片

f[i][j] = max(f[i-1][j],f[i-1][j-k*v[i]]+k*w[i])       k = 0, 1, 2, ………,s[i]

采用二进制优化方式 O ( N V log ⁡ S ) O(NV \log S) O(NVlogS)

#include
#include
#include

using namespace std;
const int MAXN = 25000, M = 2010;
int N, V, cnt;
int v[MAXN], w[MAXN], dp[M];

int main()
{
    scanf("%d%d",&N,&V);
    for(int i = 1;i <= N;i++)
    {
        int a, b, num;
        scanf("%d%d%d",&a,&b,&num);
        int k = 1;
        while(k <= num)
        {
            cnt++;
            v[cnt] = a * k;
            w[cnt] = b * k;
            num -= k;
            k *= 2;
        }
        if(num > 0)
        {
            cnt++;
            v[cnt] = a * num;
            w[cnt] = b * num;
        }
    }
    
    for(int i = 1;i <= cnt;i++)
    {
        for(int j = V;j >= v[i];j--)
        {
            dp[j] = max(dp[j],dp[j-v[i]]+w[i]);
        }
    }
    
    printf("%d\n",dp[V]);
    
    return 0;
}

分组背包

image-20210705122108140
#include
#include
#include
using namespace std;
const int MAXN = 110;
int N, V;
int s[MAXN], w[MAXN][MAXN], v[MAXN][MAXN], dp[MAXN][MAXN];

int main()
{
    scanf("%d%d",&N,&V);
    for(int i = 1;i <= N;i++)
    {
        scanf("%d",&s[i]);
        for(int j = 1;j <= s[i];j++)
        {
            scanf("%d%d",&v[i][j],&w[i][j]);
        }
    }
    
    for(int i = 1;i <= N;i++)
    {
        for(int j = 0;j <= V;j++)
        {
            dp[i][j] = dp[i-1][j];
            for(int k = 1;k <= s[i];k++)
            {
                if(j >= v[i][k]) dp[i][j] = max(dp[i][j], dp[i-1][j-v[i][k]]+w[i][k]);
            }
        }
    }
    
    printf("%d\n",dp[N][V]);
    return 0;
}
#include
#include
#include

using namespace std;
const int MAXN = 110;
int N, V;
int v[MAXN][MAXN], w[MAXN][MAXN], s[MAXN], dp[MAXN];


int main()
{
    scanf("%d%d",&N,&V);
    for(int i = 1;i <= N;i++)
    {
        scanf("%d",&s[i]);
        for(int j = 1;j <= s[i];j++)
        {
            scanf("%d%d",&v[i][j],&w[i][j]);
        }
    }
    
    for(int i = 1;i <= N;i++)
    {
        for(int j = V;j >= 0;j--)
        {
            for(int k = 1; k <= s[i];k++)
            {
                if(j >= v[i][k]) dp[j] = max(dp[j],dp[j-v[i][k]]+w[i][k]);
            }
        }
    }
    
    printf("%d\n",dp[V]);
    
    return 0;
}

线性DP

898. 数字三角形 - AcWing题库

image-20210705160106289
#include
#include
#include

using namespace std;

const int MAXN = 510;
int a[MAXN][MAXN], dp[MAXN][MAXN];
int n;

int main()
{
    scanf("%d",&n);
    for(int i = 1;i <= n;i++)
    {
        for(int j = 1;j <= i;j++)
        {
            scanf("%d",&a[i][j]);
        }
    }
    dp[1][1] = a[1][1];
    for(int i = 2;i <= n;i++)
    {
        for(int j = 1;j <= i;j++)
        {
            if(j == 1) dp[i][j] = dp[i-1][j] + a[i][j];
            else if(j == i) dp[i][j] = dp[i-1][j-1] + a[i][j];
            else dp[i][j] = max(dp[i-1][j],dp[i-1][j-1])+a[i][j];
        }
    }
    int ans = dp[n][1];
    for(int i = 1;i <= n;i++) ans = max(ans,dp[n][i]);
    printf("%d\n",ans);
    return 0;
}

895. 最长上升子序列 - AcWing题库

image-20210705164059934
#include
#include
#include
using namespace std;

const int MAXN = 1010;
int n;
int a[MAXN], dp[MAXN];

int main()
{
    scanf("%d",&n);
    for(int i = 1;i <= n;i++) scanf("%d",&a[i]);
    for(int i = 1;i <= n;i++)
    {
        dp[i] = 1;
        for(int j = 1;j < i;j++)
        {
            if(a[i] > a[j]) dp[i] = max(dp[i], dp[j] + 1);
        }
    }
    int ans = dp[n];
    for(int i = 1;i <= n;i++) ans = max(ans,dp[i]);
    printf("%d\n",ans);
    return 0;
}

O ( log ⁡ n ) O(\log n) O(logn)​优化

896. 最长上升子序列 II - AcWing题库

#include
#include
#include
#include

using namespace std;
const int MAXN = 1e5+50;
int a[MAXN], b[MAXN], n, id;

int main()
{
    scanf("%d",&n);
    for(int i = 1;i <= n;i++)
    {
        scanf("%d",&a[i]);
    }
    b[id++] = a[1];
    for(int i = 2;i <= n;i++)	// 遍历每个数
    {
        if(a[i] <= b[id-1])
        {
            int pos = lower_bound(b,b+id,a[i])-b;	// 二分找到第一个大于等于它的数进行替换
            b[pos] = a[i];
        }
        else b[id++] = a[i];
    }
    printf("%d\n",id);
    return 0;
}

897. 最长公共子序列 - AcWing题库

image-20210705193600155
#include
#include
#include
#include
using namespace std;

int n, m;
const int MAXN = 1005;
int dp[MAXN][MAXN];

int main()
{
    scanf("%d%d",&n,&m);
    char s[MAXN], t[MAXN];
    cin >> s+1 >> t+1;
    for(int i = 1;i <= n;i++)
    {
        for(int j = 1;j <= m;j++)
        {
            if(s[i] == t[j]) dp[i][j] = dp[i-1][j-1]+1;
            else dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
        }
    }
    int ans = dp[n][m];
    printf("%d\n",ans);
    return 0;
}

902. 最短编辑距离 - AcWing题库

#include
#include
#include
#include

using namespace std;

const int MAXN = 1005;
int dp[MAXN][MAXN];
char s[MAXN], t[MAXN];
int n, m;

int min_3(int a, int b, int c)
{
    int tmp = min(a, b);
    return min(c,tmp);
}

int main()
{
    scanf("%d",&n);
    cin >> s+1;
    scanf("%d",&m);
    cin >> t+1;
    
    // 注意初始化
    for(int i = 1;i <= n;i++) dp[i][0] = i;
    for(int j = 1;j <= m;j++) dp[0][j] = j;
    
    for(int i = 1;i <= n;i++)
    {
        for(int j = 1;j <= m;j++)
        {
            if(s[i] == t[j]) dp[i][j] = dp[i-1][j-1];
            else dp[i][j] = min_3(dp[i-1][j],dp[i][j-1],dp[i-1][j-1])+1;
        }
    }
    int ans = dp[n][m];
    printf("%d\n",ans);
    return 0;
}

区间DP

282. 石子合并 - AcWing题库

image-20210705202613469

保证每次区间之内所用到的数都是之前计算过的。所以首先枚举区间长度。

#include
#include
#include
#include

using namespace std;
const int MAXN = 1010;

int dp[MAXN][MAXN], a[MAXN], sum[MAXN];
int n;

int main()
{
    scanf("%d",&n);
    for(int i = 1;i <= n;i++) {
        scanf("%d",&a[i]);
        sum[i] = sum[i-1]+a[i];
    }
    for(int i = 1;i <= n;i++) dp[i][i] = 0;
    
    for(int len = 2;len <= n;len++)
    {
        for(int i = 1;i + len - 1 <= n;i++)
        {
            int l = i, r = i+len-1;
            dp[l][r] = dp[l][l]+dp[l+1][r]+sum[r]-sum[l-1];
            for(int k = l;k < r;k++)
            {
                dp[l][r] = min(dp[l][r],dp[l][k]+dp[k+1][r]+sum[r]-sum[l-1]);
            }
        }
    }
    int ans = dp[1][n];
    printf("%d\n",ans);
    return 0;
}

计数DP

900. 整数划分 - AcWing题库

#include
#include
#include
#include

using namespace std;
typedef long long ll;
const int MOD = 1e9+7;
const int MAXN = 1005;
int n;
ll dp[MAXN][MAXN];

int main()
{
    scanf("%d",&n);
    for(int i = 1;i <= n;i++) dp[n][1] = 1;
    for(int i = 1;i <= n;i++)
    {
        for(int j = 1;j <= n;j++)
        {
            if(j == i) dp[i][j] = (1 + dp[i][j-1]) % MOD;
            else if(i < j) dp[i][j] = dp[i][i];
            else dp[i][j] = (dp[i-j][j] + dp[i][j-1]) % MOD;
        }
    }
    printf("%d\n",dp[n][n]);
    return 0;
}

数位DP

分情况讨论

[a,b] 0~9出现的次数
count(n,x) 表示[1,n]中x出现的次数
count(b,x)-count(a-1,x)
// 分情况讨论

状压DP

291. 蒙德里安的梦想 - AcWing题库

当所有横向小方格都放完了,纵向小方格只有一种摆法。

故所有的摆放方式都和横向摆放小方格的摆放方式一样。

f[i][j]表示第i列是否有从前一列伸出的横向小方格。image-20210707120217403

#include
#include
#include
#include

using namespace std;
typedef long long ll;
ll dp[12][3000];
bool vis[3000];

int main()
{
    while(true)
    {
        int n, m;
        scanf("%d%d",&n,&m);
        if(n == 0 && m == 0) break;
        memset(dp,0,sizeof(dp));
        for(int i = 0;i < 1 << n;i++)
        {
            vis[i] = true;
            int cnt = 0;
            for(int j = 0;j < n;j++)
            {
                if((i >> j) & 1)	// 注意运算顺序
                {
                    if(cnt % 2 == 1) vis[i] = false;
                }
                else cnt++;
            }
            if(cnt % 2 == 1) vis[i] = false;
        }
        dp[0][0] = 1;
        for(int i = 1;i <= m;i++)
        {
            for(int j = 0;j < (1<

91. 最短Hamilton路径 - AcWing题库

image-20210707143854787
#include
#include
#include
#include

using namespace std;

const int MAXN = 1 << 20;
const int INF = 0x3f3f3f3f;
int w[21][21], dp[MAXN][21];
int n;

int main()
{
    scanf("%d",&n);
    for(int i = 0;i < n;i++)
    {
        for(int j = 0;j < n;j++)
        {
            scanf("%d",&w[i][j]);
        }
    }
    
    memset(dp,INF,sizeof(dp));
    
    dp[1][0] = 0;
    
    for(int i = 0;i < (1<> j) & 1)
            {
                for(int k = 0;k < n;k++)
                {
                    if((i >> k) & 1)
                    dp[i][j] = min(dp[i][j],dp[i-(1 << j)][k] + w[k][j]);
                }
            }
        }
    }
    
    int ans = dp[(1 << n) - 1][n-1];
    printf("%d\n",ans);
    
    return 0;
}

树形DP

image-20210707173629825
// u的子树为s1,s2
f[u,0] = f[u,0] + max(f[s1,0],f[s1,1]) + max(f[s2,0],f[s2,1])
f[u,1] = f[u,1] + f[s1,0] + f[s2,0]

285. 没有上司的舞会 - AcWing题库

#include
#include
#include
#include

using namespace std;
typedef long long ll;
const int MAXN = 6010;
int n, id, head[MAXN], dp[MAXN][2], happy[MAXN];
struct Edge{
    int to, next;
}edge[MAXN];
bool has_father[MAXN];

void addedge(int a, int b)
{
    edge[id].to = b;
    edge[id].next = head[a];
    head[a] = id++;
    return;
}

void dfs(int u)
{
    dp[u][1] = happy[u];
    
    for(int i = head[u];i != -1;i = edge[i].next)
    {
        int p = edge[i].to;
        dfs(p);
        
        dp[u][1] += dp[p][0];
        dp[u][0] += max(dp[p][0],dp[p][1]);
    }
}

int main()
{
    memset(head,-1,sizeof(head));
    scanf("%d",&n);
    for(int i = 1;i <= n;i++) scanf("%d",&happy[i]);
    
    for(int i = 1;i <= n-1;i++)
    {
        int a, b;
        scanf("%d%d",&a,&b);
        addedge(b,a);
        has_father[a] = true;
    }
    
    int root = 1;
    while(has_father[root])
    {
        root++;
    }
    dfs(root);
    int ans = max(dp[root][0],dp[root][1]);
    printf("%d\n",ans);
    
    return 0;
}

记忆化搜索

901. 滑雪 - AcWing题库

image-20210707194117212
#include
#include
#include
#include

using namespace std;
const int MAXN = 305;
int n, m;
int dp[MAXN][MAXN], w[MAXN][MAXN];
int dx[4] = {0,1,0,-1}, dy[4] = {1,0,-1,0};

int dfs(int x, int y)
{
    if(dp[x][y] != -1) return dp[x][y];
    dp[x][y] = 1;
    for(int i = 0;i < 4;i++)
    {
        int u = x + dx[i], v = y + dy[i];
        if(u >= 1 && u <= n && v >= 1 && v <= m && w[u][v] < w[x][y])
        {
            dp[x][y] = max(dp[x][y],1+dfs(u,v));
        }
    }
    return dp[x][y];
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++)
    {
        for(int j = 1;j <= m;j++)
        {
            scanf("%d",&w[i][j]);
        }
    }
    int ans = 1;
    memset(dp,-1,sizeof(dp));
    for(int i = 1;i <= n;i++)
    {
        for(int j = 1;j <= m;j++)
        {
            ans = max(ans,dfs(i,j));
        }
    }
    printf("%d\n",ans);
    return 0;
}

6. 贪心

区间问题

905. 区间选点 - AcWing题库

#include
#include
#include
using namespace std;

const int MAXN = 1e5+50;
int n;
struct node{	// 按照右端点进行排序
    int l, r;
    bool operator<(const node &a)
    {
        if(r == a.r) return l < a.l;
        return r < a.r;
    }
}Node[MAXN];

int main()
{
    scanf("%d",&n);
    for(int i = 1;i <= n;i++)
    {
        scanf("%d%d",&Node[i].l,&Node[i].r);
    }
    sort(Node+1,Node+n+1);
    int cnt = 0;
    int r;
    for(int i = 1;i <= n;i++)
    {
        if(!cnt || Node[i].l > r) {
            r = Node[i].r;
            cnt++;
        }
    }
    printf("%d\n",cnt);
    return 0;
}

908. 最大不相交区间数量 - AcWing题库

#include
#include
#include

using namespace std;

const int MAXN = 1e5+50;
// 按照右端点排序
struct node{
    int l, r;
    bool operator<(const node &a)
    {
        if(r == a.r) return l < a.l;
        return r < a.r;
    }
}Node[MAXN];
int n;

int main()
{
    scanf("%d",&n);
    for(int i = 1;i <= n;i++) scanf("%d%d",&Node[i].l,&Node[i].r);
    sort(Node+1,Node+1+n);
    int cnt = 0, r;
    for(int i = 1;i <= n;i++)
    {
        if(!cnt || Node[i].l > r)
        {
            r = Node[i].r;
            cnt++;
        }
    }
    printf("%d\n",cnt);
    return 0;
}

906. 区间分组 - AcWing题库

#include
#include
#include
#include
#include

using namespace std;

const int MAXN = 1e5+50;
struct node{
    int l, r;
    bool operator<(const node &a)
    {
        if(l == a.l) return r < a.r;
        return l < a.l;
    }
}Node[MAXN];
int n;

priority_queue,greater>pq;

int main()
{
    scanf("%d",&n);
    for(int i = 0;i < n;i++)
    {
        scanf("%d%d",&Node[i].l,&Node[i].r);
    }
    sort(Node,Node+n);
    int cnt = 0;
    for(int i = 0;i < n;i++)
    {
        if(!cnt || pq.top() >= Node[i].l)
        {
            cnt++;
            pq.push(Node[i].r);
        }
        else {
            pq.pop();
            pq.push(Node[i].r);
        }
    }
    printf("%d\n",cnt);
    return 0;
}

把所有开始时间和结束时间排序,遇到开始时间就把需要的教室加1,遇到结束时间就把需要的教室减1,在一系列需要的教室个数变化的过程中,峰值就是多同时进行的活动数,也是我们至少需要的教室数。

#include
#include
#include
#include

using namespace std;
const int MAXN = 1e5+10;
int n;
int b[2*MAXN], idx;

int main()
{
    scanf("%d",&n);
    for(int i = 0;i < n;i++)
    {
        int l, r;
        scanf("%d%d",&l,&r);
        b[idx++] = 2*l;	// 进行适当的奇偶变换
        b[idx++] = 2*r+1;	// 偶数为了避免重复加上一
    }
    sort(b,b+idx);
    int t = 0, ans = 0;
    for(int i = 0;i < idx;i++)
    {
        if(b[i] % 2 == 0) t++;
        else t--;
        ans = max(ans,t);
    }
    printf("%d\n",ans);
    
    return 0;
}

AcWing 907. 区间覆盖 - AcWing

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iskX8AxW-1629167397792)(C:/Users/DELL/AppData/Roaming/Typora/typora-user-images/image-20210706105409443.png)]

#include
#include
#include
#include

using namespace std;

const int MAXN = 1e5+50;
struct node{
    int l, r;
    bool operator<(const node &a)	// 按照左端点排序
    {
        return l < a.l;
    }
}Node[MAXN];
int n, le, ri;

int main()
{
    bool flag = true;
    scanf("%d%d",&le,&ri);
    scanf("%d",&n);
    for(int i = 0;i < n;i++) scanf("%d%d",&Node[i].l,&Node[i].r);
    sort(Node,Node+n);
    int start = le, tmp = le, cnt = 0;
    for(int i = 0;i < n;)
    {
        if(start >= ri) break;	// 超过区间退出
        while(Node[i].l <= start && i < n)
        {
            tmp = max(tmp,Node[i].r);	// 固定左端点,找最大的右端点
            i++;
        }
        cnt++;
        if(tmp == start) {
            flag = false;
            break;
        }
        start = tmp;
    }
    if(start < ri) flag = false;	// 未到区间长度记为false
    if(flag) printf("%d\n",cnt);
    else printf("-1\n");
    return 0;
}

Huffman树

148. 合并果子 - AcWing题库

#include
#include
#include
#include
#include

using namespace std;
const int MAXN = 10010;
int n;
int a[MAXN];
priority_queue,greater>pq;

int main()
{
    scanf("%d",&n);
    for(int i = 0;i < n;i++) {
        int x;
        scanf("%d",&x);
        pq.push(x);
    }
    int ans = 0;
    for(int i = 0;i < n-1;i++)
    {
        int u = pq.top();
        pq.pop();
        int v = pq.top();
        pq.pop();
        ans += u+v;
        pq.push(u+v);
    }
    printf("%d\n",ans);
    return 0;
}

排序不等式

活动 - AcWing

#include
#include
#include
#include
using namespace std;
typedef long long ll;
int n;
const int MAXN = 100010;
int a[MAXN];
int main()
{
    scanf("%d",&n);
    for(int i = 0;i < n;i++) scanf("%d",&a[i]);
    sort(a,a+n);	// 按照打水短的先打水
    ll ans = 0, sum = 0;
    for(int i = 0;i < n;i++)
    {
        ans += sum;
        sum += a[i];
    }
    printf("%lld\n",ans);
}

绝对值不等式

104. 货仓选址 - AcWing题库

#include
#include
#include
#include
using namespace std;
typedef long long ll;
const int MAXN = 100010;
int a[MAXN];
int n;
int main()
{
    scanf("%d",&n);
    for(int i = 1;i <= n;i++) {
        scanf("%d",&a[i]);
    }
    sort(a+1,a+n+1);
    int mid = a[n/2+1];	// 取中间的数作为建站点
    ll ans = 0;
    for(int i = 1;i <= n;i++) ans += abs(mid-a[i]);
    printf("%d\n",ans);
    return 0;
}

推公式

那么我们要求的就是找出一种牛的排列方式,令 m a x ( w 1 + ⋅ ⋅ ⋅ + w i − 1 − s i ) max(w_1+⋅⋅⋅+w_{i−1}−s_i) max(w1++wi1si)最小,记这个值为val。

为了求排序的方案,可以交换i,i+1牛的位置,看看满足什么等价条件,就可以使得交换之后val不比之前大。

image-20210706172531047

由于s, w都是正数, w i − s i + 1 > − s i + 1 w_i−s_{i+1}>−s_{i+1} wisi+1>si+1 , w i + 1 − s i > − s i w_{i+1}−s_i>−s_i wi+1si>si
比较 w i − s i + 1 w_i−s_{i+1} wisi+1 w i + 1 − s i w_{i+1}−s_{i} wi+1si即可

所以得到做法: 按每头牛的 w + s 进行排序, 当存在逆序时就进行交换(即升序排序),
然后根据题意算出每头牛的危险值记录其中的最大值即可

125. 耍杂技的牛 - AcWing题库

#include
#include
#include

using namespace std;
typedef long long ll;
int n;
const int MAXN = 50050;
ll sum, ans;
struct Cow{
    int s, w;
    bool operator<(const Cow &a)
    {
        return s+w < a.s+a.w;
    }
}cow[MAXN];

int main()
{
    scanf("%d",&n);
    for(int i = 0;i < n;i++)
    {
        scanf("%d%d",&cow[i].w,&cow[i].s);
    }
    sort(cow,cow+n);
    ans -= cow[0].s;
    sum = cow[0].w;
    for(int i = 1;i < n;i++)
    {
        if(sum - cow[i].s > ans) ans = sum - cow[i].s;
        sum += cow[i].w;
    }
    printf("%lld\n",ans);
    return 0;
}

7.时空复杂度分析

一般ACM或者笔试题的时间限制是1秒或2秒。
在这种情况下,C++代码中的操作次数控制在 1 0 7 ∼ 1 0 8 10^7∼10^8 107108 为最佳。

下面给出在不同数据范围下,代码的时间复杂度和算法该如何选择:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y0yz0FNM-1629167397793)(C:/Users/DELL/AppData/Roaming/Typora/typora-user-images/image-20210706175918298.png)]

ode[i].l,&Node[i].r);
}
sort(Node,Node+n);
int cnt = 0;
for(int i = 0;i < n;i++)
{
if(!cnt || pq.top() >= Node[i].l)
{
cnt++;
pq.push(Node[i].r);
}
else {
pq.pop();
pq.push(Node[i].r);
}
}
printf("%d\n",cnt);
return 0;
}




把所有开始时间和结束时间排序,遇到开始时间就把需要的教室加1,遇到结束时间就把需要的教室减1,在一系列需要的教室个数变化的过程中,峰值就是多同时进行的活动数,也是我们至少需要的教室数。



```c++
#include
#include
#include
#include

using namespace std;
const int MAXN = 1e5+10;
int n;
int b[2*MAXN], idx;

int main()
{
    scanf("%d",&n);
    for(int i = 0;i < n;i++)
    {
        int l, r;
        scanf("%d%d",&l,&r);
        b[idx++] = 2*l;	// 进行适当的奇偶变换
        b[idx++] = 2*r+1;	// 偶数为了避免重复加上一
    }
    sort(b,b+idx);
    int t = 0, ans = 0;
    for(int i = 0;i < idx;i++)
    {
        if(b[i] % 2 == 0) t++;
        else t--;
        ans = max(ans,t);
    }
    printf("%d\n",ans);
    
    return 0;
}

AcWing 907. 区间覆盖 - AcWing

[外链图片转存中…(img-iskX8AxW-1629167397792)]

#include
#include
#include
#include

using namespace std;

const int MAXN = 1e5+50;
struct node{
    int l, r;
    bool operator<(const node &a)	// 按照左端点排序
    {
        return l < a.l;
    }
}Node[MAXN];
int n, le, ri;

int main()
{
    bool flag = true;
    scanf("%d%d",&le,&ri);
    scanf("%d",&n);
    for(int i = 0;i < n;i++) scanf("%d%d",&Node[i].l,&Node[i].r);
    sort(Node,Node+n);
    int start = le, tmp = le, cnt = 0;
    for(int i = 0;i < n;)
    {
        if(start >= ri) break;	// 超过区间退出
        while(Node[i].l <= start && i < n)
        {
            tmp = max(tmp,Node[i].r);	// 固定左端点,找最大的右端点
            i++;
        }
        cnt++;
        if(tmp == start) {
            flag = false;
            break;
        }
        start = tmp;
    }
    if(start < ri) flag = false;	// 未到区间长度记为false
    if(flag) printf("%d\n",cnt);
    else printf("-1\n");
    return 0;
}

Huffman树

148. 合并果子 - AcWing题库

#include
#include
#include
#include
#include

using namespace std;
const int MAXN = 10010;
int n;
int a[MAXN];
priority_queue,greater>pq;

int main()
{
    scanf("%d",&n);
    for(int i = 0;i < n;i++) {
        int x;
        scanf("%d",&x);
        pq.push(x);
    }
    int ans = 0;
    for(int i = 0;i < n-1;i++)
    {
        int u = pq.top();
        pq.pop();
        int v = pq.top();
        pq.pop();
        ans += u+v;
        pq.push(u+v);
    }
    printf("%d\n",ans);
    return 0;
}

排序不等式

活动 - AcWing

#include
#include
#include
#include
using namespace std;
typedef long long ll;
int n;
const int MAXN = 100010;
int a[MAXN];
int main()
{
    scanf("%d",&n);
    for(int i = 0;i < n;i++) scanf("%d",&a[i]);
    sort(a,a+n);	// 按照打水短的先打水
    ll ans = 0, sum = 0;
    for(int i = 0;i < n;i++)
    {
        ans += sum;
        sum += a[i];
    }
    printf("%lld\n",ans);
}

绝对值不等式

104. 货仓选址 - AcWing题库

#include
#include
#include
#include
using namespace std;
typedef long long ll;
const int MAXN = 100010;
int a[MAXN];
int n;
int main()
{
    scanf("%d",&n);
    for(int i = 1;i <= n;i++) {
        scanf("%d",&a[i]);
    }
    sort(a+1,a+n+1);
    int mid = a[n/2+1];	// 取中间的数作为建站点
    ll ans = 0;
    for(int i = 1;i <= n;i++) ans += abs(mid-a[i]);
    printf("%d\n",ans);
    return 0;
}

推公式

那么我们要求的就是找出一种牛的排列方式,令 m a x ( w 1 + ⋅ ⋅ ⋅ + w i − 1 − s i ) max(w_1+⋅⋅⋅+w_{i−1}−s_i) max(w1++wi1si)最小,记这个值为val。

为了求排序的方案,可以交换i,i+1牛的位置,看看满足什么等价条件,就可以使得交换之后val不比之前大。

image-20210706172531047

由于s, w都是正数, w i − s i + 1 > − s i + 1 w_i−s_{i+1}>−s_{i+1} wisi+1>si+1 , w i + 1 − s i > − s i w_{i+1}−s_i>−s_i wi+1si>si
比较 w i − s i + 1 w_i−s_{i+1} wisi+1 w i + 1 − s i w_{i+1}−s_{i} wi+1si即可

所以得到做法: 按每头牛的 w + s 进行排序, 当存在逆序时就进行交换(即升序排序),
然后根据题意算出每头牛的危险值记录其中的最大值即可

125. 耍杂技的牛 - AcWing题库

#include
#include
#include

using namespace std;
typedef long long ll;
int n;
const int MAXN = 50050;
ll sum, ans;
struct Cow{
    int s, w;
    bool operator<(const Cow &a)
    {
        return s+w < a.s+a.w;
    }
}cow[MAXN];

int main()
{
    scanf("%d",&n);
    for(int i = 0;i < n;i++)
    {
        scanf("%d%d",&cow[i].w,&cow[i].s);
    }
    sort(cow,cow+n);
    ans -= cow[0].s;
    sum = cow[0].w;
    for(int i = 1;i < n;i++)
    {
        if(sum - cow[i].s > ans) ans = sum - cow[i].s;
        sum += cow[i].w;
    }
    printf("%lld\n",ans);
    return 0;
}

7.时空复杂度分析

一般ACM或者笔试题的时间限制是1秒或2秒。
在这种情况下,C++代码中的操作次数控制在 1 0 7 ∼ 1 0 8 10^7∼10^8 107108 为最佳。

下面给出在不同数据范围下,代码的时间复杂度和算法该如何选择:

[外链图片转存中…(img-Y0yz0FNM-1629167397793)]

你可能感兴趣的:(ACWing算法基础课)