算法笔记(个人用)(不定期更新)

文章目录

前言说明

在线查看博客: 算法笔记(个人用)(不定期更新)_CUBE_lotus的博客-CSDN博客

博客主页:CUBE_lotus

哔哩哔哩:天赐细莲

交流email : 1539349804@qq.com

主攻OJ平台:天赐细莲 - 力扣(LeetCode) 欢迎来交流

本文导出时间:2022年8月8日

开源算法模板-github

Note

文档编辑相关

Ubuntu Pastebin

Cmd Markdown 公式指导手册

语言相关

输入输出流

// 提高cin cout 速度

用这个不如老老实实判断数据范围用 scanfprintf

std::ios::sync_with_stdio(false);

随机数

// 传统C语言式 
srand((unsigned)time(NULL));
int r = rand();

// C++11
mt19937 gen{random_device{}()};
uniform_int_distribution<int> dis(-100, 114514);
int r = dis(gen);

二分查找函数

  • lower_bound 第一个 ≥ ≥ 的数值 返回迭代器

  • upper_bound 第一个 > > > 的数值 返回迭代器

  • binary_search 单纯的二分查找 返回bool

一些宏定义

  • RAND_MAX 随机数最大值,一般为 0x7fffffff0x7fff
  • M_PI 圆周率 3.14159265358979323846

算法相关

时间复杂度

下表为y总整理

出处:由数据范围反推算法复杂度以及算法内容 - AcWing

数据范围算法
n ≤ 30 {n \le 30} n30指数级别,dfs+剪枝,状压dp
n ≤ 1 e 2 = = > O ( n 3 ) {n \le 1e2 ==> O(n^3)} n1e2==>O(n3)floyd,dp, 高斯消元
n ≤ 1 e 3 = = > O ( n 2 ) , O ( n 2 l o g n ) {n \le 1e3 ==> O(n^2), O(n^2logn)} n1e3==>O(n2),O(n2logn)dp, 二分, 朴素dijkstra,朴素prim, bellman-ford
n ≤ 1 e 4 = = > O ( n ∗ n ) {n \le 1e4 ==> O(n*\sqrt{n})} n1e4==>O(nn )块状链表, 分块, 莫队
n ≤ 1 e 5 = = > O ( n l o g n ) {n \le 1e5 ==> O(nlogn)} n1e5==>O(nlogn)各种sort, 线段树, 树状数组,set/map, heap, 拓扑排序,堆优化dijkstra
n ≤ 1 e 6 = = > O ( n ) , 常数小 O ( n l o g n ) {n \le 1e6 ==> O(n), 常数小O(nlogn)} n1e6==>O(n),常数小O(nlogn)单调队列, hash, 双指针, 并查集, kmp, AC自动机
sort, 树状数组,heap, dijstra, spfa
n ≤ 1 e 7 = = > O ( n ) {n \le 1e7 ==> O(n)} n1e7==>O(n)双指针, kmp, AC自动机, 线性素数筛
n ≤ 1 e 9 = = > O ( n ) {n \le 1e9 ==> O(\sqrt{n})} n1e9==>O(n )素数判断
n ≤ 1 e 18 = = > O ( l o g n ) {n \le 1e18 ==> O(logn)} n1e18==>O(logn)最大公约数, 快速幂
n ≤ 1 e 1000 = = > O ( ( l o g n ) 2 ) {n \le 1e1000 ==> O((logn)^2)} n1e1000==>O((logn)2)高精度加减乘除
n ≤ 1 e 100000 = = > O ( l o g k ∗ l o g l o g k ) {n \le 1e100000 ==> O(logk * loglogk)} n1e100000==>O(logkloglogk)(k表示位数) 高精度加减, FFT/NTT

排序

(排序) 各种排序算法汇总_天赐细莲的博客-CSDN博客

题单

注意以下单非本人整理

kuangbin:

ACM新模板 | kuangbin的博客

[kuangbin带你飞]专题1-23 - Virtual Judge (csgrandeur.cn)

牛客:

【新手上路】语法入门&算法入门题单_牛客

【237题】算法基础精选题单 牛客

宫水三叶(力扣为主):

Home · SharingSource/LogicStack-LeetCode Wiki · GitHub

ReseeCher(洛谷):

能力全面提升综合题单 - 题单 - 洛谷


优秀讲师:

董晓算法 哔哩哔哩

基础数学

素数

专题:(数论) 从判断素数到素数筛_CUBE_lotus的博客-CSDN博客

蔡勒(Zeller)公式

[ ] 表示取整 []{表示取整} []表示取整

w 星期, 0 到 7 ,周日到周一 w{星期, 0到7,周日到周一} w星期,07,周日到周一

c 年份的前两位 c{年份的前两位} c年份的前两位

y 年份后两位 y{年份后两位} y年份后两位

m 月份, 3 到 14 ( 1 , 2 月化为前一年的 13 , 14 月) m{月份,3到14(1,2月化为前一年的13,14月)} m月份,31412月化为前一年的1314月)

d 日 d{日} d

w = ( [ c 4 ] − 2 c + y + [ y 4 ] + [ 13 ∗ ( m + 1 ) 5 ] + d − 1 ) m o d 7 w = ([\frac{c}{4}] - 2c + y + [\frac{y}{4}] + [\frac{13*(m+1)}{5}] + d -1 ) mod 7 w=([4c]2c+y+[4y]+[513(m+1)]+d1)mod7

力扣:1185. 一周中的第几天

class Solution {
private:
    const vector<string> WEEK = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
public:
    string dayOfTheWeek(int day, int month, int year) {
        int m = month;
        if (m <= 2) {
            m += 12;
            year--;
        } 
        int c = year/100;
        int y = year%100;        

        long w = (c/4) - 2*c + y + (y/4) + 13*(m+1)/5 + day - 1;
        // 可能出现负数 最小值是199 则取一个比199大的三的倍数
        w = (w + 210) % 7;
        return WEEK[w];
    }
};

10转R进制

练习题:

力扣:504. 七进制数

力扣:405. 数字转换为十六进制数 补码

class Solution {
public:
    string convertToBase7(int num) {
        return tenToR(num, 7);
    }
private:
    const string words = "0123456789abcdef";
    string tenToR(int num, const int R) {
        // 特判0
        if (num == 0) {
            return "0";
        }
        // 处理负数
        long long n = num;
        string flag;
        if (n < 0) {
            flag = "-";
            n = -n;
        }

        string s;
        while(n) {
            int idx = n%R;
            n /= R;
            s += words[idx];
        }
        reverse(s.begin(), s.end());

        return flag + s;
    }
};

卡特兰数

1.卡特兰数是一种数列,以比利时的数学家欧仁·查理·卡塔兰命名。

2.卡特兰数列:1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862, 16796, 58786, 208012……

令第n项为h(n),则:

h(0) = 1;

h(1) = 1;

h(n) = h(0)*h(n-1) + h(1)*h(n-2) + …… + h(n-1)*h(0),其中n>=2

卡特兰数的另一种形式:h(n) = C(2n,n)/(n + 1),其中n>=1

不要用h(n) = C(2n,n)/(n + 1)这个的。

int n;
ll f1[maxn], f2[maxn];
ll f[maxn * 2][maxn];
//公式1:
int solve1() {
    f1[0] = f1[1] = 1;
    cin >> n;
    for(int i = 2; i <= n; i++) {
        for(int j = 0; j < i; j++)
            f1[i] += (f1[j] * f1[i-j-1]);   //f(n)=f(0)f(n-1)+f(1)f(n-2)+...+f(n-1)f(0)
    }
    printf("%lld\n",f1[n]);
    return 0;
}
//公式2:
int solve2() {
    f2[0] = f2[1] = 1;
    cin >> n;
    for(int i = 2; i <= n; i++)
        f2[i] += f2[i - 1] * (4 * i - 2) / (i + 1);  //f(n)=f(n-1)*(4*n-2)/(n+1)
    printf("%lld\n", f2[n]);
    return 0;
}
//公式3:
int solve3() {
    cin >> n;
    for(int i = 1; i <= 2 * n; i++) {
        f[i][0] = f[i][i] = 1;
        for(int j = 1; j < i; j++)
            f[i][j] = f[i - 1][j] + f[i - 1][j - 1];     //f(n)=c(2n,n)/(n+1)
    }
    printf("%lld",f[2 * n][n] / (n + 1));
    return 0;
}

完美数

对于一个 正整数,如果它和除了它自身以外的所有 正因子 之和相等,我们称它为 「完美数」。

分解因数

力扣: 507. 完美数

class Solution {
public:
    bool checkPerfectNumber(int num) {
        if (num == 1) {
            return false;
        }
        int root = (int)sqrt(num);

        int sum = 0;
        for (int i = 1; i <= root; i++) {
            if (num%i == 0) {
                sum += i;
                sum += num/i;
            }
        }
        sum -= num;
        return sum == num;
    }
};

欧几里得-欧拉定理

根据欧几里得-欧拉定理,每个偶完全数都可以写成
2 p − 1 ( 2 p − 1 ) 2^{p-1}(2^p-1) 2p1(2p1)
的形式,其中 p p{} p 为素数且 2 p − 1 2^p-1{} 2p1 为素数。

class Solution {
public:
    bool checkPerfectNumber(int num) {
        return num == 6 || num == 28 || num == 496 || num == 8128 || num == 33550336;
    }
};

数根

数根_百度百科 (baidu.com)

将一正整数的各个位数相加(即横向相加)后,若加完后的值大于等于10的话,则继续将各位数进行横向相加直到其值小于十为止所得到的数,即为数根。换句话说,数根是将一数字重复做其数字之和,直到其值小于十为止,则所得的值为该数的数根。例如54817的数根为7,因为5+4+8+1+7=25,25大于10则再加一次,2+5=7,7小于十,则7为54817的数根。

涉及知识: 同余原理

力扣:258. 各位相加

class Solution {
public:
    int addDigits(int num) {
        if (num == 0) {
            return num;
        }
        return num%9 ? num%9 : 9;
        return (num-1)%9 + 1;
    }
};

[1~n] 中完全平方数的个数

原数:1,2,3,4,5 ······

平方:1,4,9,16,25 ······

则已知数 num 必然在一个区间[m^2, (m+1)^2) 之间

即:m^2 <= num < (m+1)^2

[1~n] 中完全平方数的个数 == floor(sqrt(n))

力扣:319. 灯泡开关

class Solution {
public:
    int bulbSwitch(int n) {
        // 避免浮点数问题
        // 如sqrt(4) = 1.999
        // int(sqrt(4)) = 1
        return sqrt(n + 0.5);
    }
};

容斥原理

杭电:今年暑假不AC - 2037

/**
杭电 今年暑假不AC
https://acm.hdu.edu.cn/showproblem.php?pid=2037
排序 + 容斥原理
贪心算法
    1.先把每项按照结束时间,从小到大排序
    2.优先选择时间先结束的(并且与前面不冲突)
*/ 
#include<bits/stdc++.h>
using namespace std;

#define from first
#define to second

using pii = pair<int, int>;

void solve(int n) {
    vector<pii> arr(n);
    for (int i = 0; i < n; i++) {
        cin >> arr[i].from >> arr[i].to;
    }

    // 按照先达到的时间排序,短棒(起始时间慢的)优先
    sort(arr.begin(), arr.end(), [](const pii& a, const pii& b){
        return (a.to != b.to) ? (a.to < b.to) : (a.from > b.from);
    });

    int ans = 0;
    int pre = 0;
    for (int i = 0; i < n; i++) {
        int from = arr[i].from;
        int to = arr[i].to;
        // 但前起始时间在前一轮的终止时间之后,则可计算
        if (from >= pre) {
            pre = to;
            ans ++;
        }
    }

    cout << ans << endl;
}

signed main (void) {
    int x = 1;
    while (cin >> x, x) {
        solve(x);
    }

    return 0;
}

借助对数防止累乘溢出

log ⁡ ∏ l = i j nums [ l ] = ∑ l = i j log ⁡ nums [ l ] \log \prod_{l=i}^{j} \textit{nums}[l] = \sum_{l=i}^{j} \log \textit{nums}[l] logl=ijnums[l]=l=ijlognums[l]

练习题:

力扣:713. 乘积小于 K 的子数组

class Solution {
public:
    int numSubarrayProductLessThanK(vector<int>& nums, int k) {
        int n = nums.size();
        double logk = log(k);

        vector<double> pre(n+1);
        for (int i = 1; i <= n; i++) {
            pre[i] = pre[i-1] + log(nums[i-1]);
        }

        int ans = 0;
        for (int right = 0; right < n; right++) {
            // + 1e-10防止精度误差
            int left = upper_bound(pre.begin(), pre.begin()+right+1, pre[right+1]-logk+1e-10) - pre.begin();
            ans += right-left+1;
        }

        return ans;
    }
};

二分查找

基础线性二分

二分的本质不是单调性,而是二段性,即一段满足某一个性质,而另一段不满足。

相关基础题目:

力扣:704. 二分查找

力扣:35. 搜索插入位置

力扣:34. 在排序数组中查找元素的第一个和最后一个位置

力扣:278. 第一个错误的版本

力扣:69. x 的平方根

逼近类二分

(二分) 逼近类二分搜索_天赐细莲的博客-CSDN博客

278. 第一个错误的版本 - (二分) 逼近类二分搜索 - 第一个错误的版本 - 力扣(LeetCode) (leetcode-cn.com)

练习题:

力扣:278. 第一个错误的版本

基于树的二分

练习题:

基于二叉搜索树(BST)的性质

左子树 < root > 右子树 (一般没有相同的值)

力扣:面试题 04.06. 后继者

class Solution {
public:
    TreeNode* inorderSuccessor(TreeNode* root, TreeNode* p) {
        TreeNode * ans = nullptr;
        // 根据BST性质,直接二分找答案
        TreeNode *cur = root;
        while (cur != nullptr) {
            if (cur->val > p->val) {
                ans = cur;
                cur = cur->left;
            } else {
                cur = cur->right;
            }
        }
        return ans;
    }
};

有序矩阵的二分

378_fig2.png (2000×1013) (leetcode-cn.com)

练习题:

力扣:378. 有序矩阵中第 K 小的元素

力扣:668. 乘法表中第k小的数

力扣:719. 找出第 K 小的数对距离

class Solution {
public:
    int kthSmallest(vector<vector<int>>& matrix, int k) {
        int n = matrix.size();
        // 走台阶式的累计方式 (也可以直接每行二分算 )
        auto check = [&](int mid) {
            int ans = 0;
            int i = 0, j = n-1;
            while (i <= n-1 && j >= 0) {
                if (matrix[i][j] <= mid) {
                    ans += j+1;
                    i++;
                } else {
                    j--;
                }
            }
            return ans >= k;
        };

        int left = matrix[0][0], right = matrix[n-1][n-1];
        while (left < right) {
            // 二分,将取余划分为左半边和右半边
            // 左半边小于等于mid,右半边大于mid
            int mid = (right-left)/2 + left;
            if (!check(mid)) {
                // 不满足小于等于有k个
                left = mid + 1;
            } else {
                // 满足小于等于k个
                right = mid;
            }
        }

        return left;
    }
};

三分查找

用于凹凸区间找最值

练习题:

力扣:462. 最少移动次数使数组元素相等 II

class Solution {
public:
    int minMoves2(vector<int>& nums) {
        auto getDiff = [&](long long diff)->long long {
            long long sum = 0;
            for_each(nums.begin(), nums.end(), [&](int &x) {
                sum += abs(diff-x);
            });
            return sum;
        };

        int left = *min_element(nums.begin(), nums.end());
        int right = *max_element(nums.begin(), nums.end());

        while (left <= right) {
            int lmid = left + (right-left)/3;
            int rmid = right - (right-left)/3;
            if (getDiff(lmid) > getDiff(rmid)) {
                left = lmid + 1;
            } else {
                right = rmid - 1;
            }
        }

        return min(getDiff(left), getDiff(right));
    }
};

位运算

整形存储大小

int

(4字节, 32位)

int -2147483648~2147483647

[ − 2 31 , 2 31 − 1 ] [-2^{31}, 2^{31}-1] [231,2311]

( − 2 ∗ e 9 , 2 ∗ e 9 ) (-2*e^9, 2*e^9) (2e9,2e9)

unsigned int 0~4294967295

[ 0 , 2 32 − 1 ] [0, 2^{32}-1] [0,2321]

[ 0 , 4 ∗ e 9 ) [0, 4*e^9) [0,4e9)

long long

(8字节,64位)

long long -9223372036854775808 ~ 9223372036854775807

[ − 2 63 , 2 63 − 1 ] [-2^{63}, 2^{63}-1] [263,2631]

( − 9 ∗ e 18 , 9 ∗ e 18 ) (-9*e^{18}, 9*e^{18}) (9e18,9e18)

unsigned long long 0 ~ 1844674407370955161

[ 0 , 2 64 − 1 ] [0, 2^{64}-1] [0,2641]

[ 0 , 1.8 ∗ 1 0 19 ) [0, 1.8*10^{19}) [0,1.81019)

__builtin_函数

// 二进制1的个数
int cnt = __builtin_popcount(x);
// 二进制前导零的个数
int cnt = __builtin_clz(x);
// 二进制后导零的个数
int cnt = __builtin_ctz(x);

取模

自动取模工具类

(C++) 基于重载运算符的自动取模_天赐细莲的博客-CSDN博客

除法取模

练习题:

杭电:A/B - 1576

/**
* molecular 分子
* denominator 分母
*/
int subMod(int molecular, int denominator, int mod) {
    auto binPow = [](int base, int expo, int mod) -> int {
        int ans = 1;
        base %= mod;
        if (expo == 0) {
            ans = 1%mod;
        } else {
            while(expo) {
                if (expo & 1) {
                    ans = ans * base % mod;
                }
                base = base * base % mod;
                expo >>= 1;
            }
        }

        return ans;
    };

    int inverseElement = binPow(denominator, mod-2, mod);
    return (molecular * inverseElement) % mod;
}

统计1的个数

力扣:191. 位1的个数

class Solution {
public:
    int hammingWeight(uint32_t n) {
        int cnt = 0;
        while(n) {
            cnt++;
            n = n&(n-1);
        }
        return cnt;
    }
};

获取末位的1

即:lowbit(x);

主要用于树状数组

非负整数n在二进制表示下最低位1及其后面的0构成的数值

EG:lowbit(44) = lowbit(101100)2 = (100)2 = 4;

n&(~n+1) == n&-n //补码

#define lowbit(x) (x)&(-1*(x))
int lowbit(int x) {
    return x&-x;
}

消除末位的1

力扣:231. 2 的幂

(此题获得末位1 和 消除末位的1 都可以做)

n = n &(n-1); //消去最后一个1

class Solution {
public:
    bool isPowerOfTwo(int n) {
        if (n <= 0) return false;
        //这里要加括号,布尔运算符 > 逻辑运算符
        return (n&(n-1)) == 0;
    }
};

判断两个数 一正一负

默认两数没有0

或者可以理解为一负,一非负

bool isOnePositiveOneNegative(int num1, int num2){
    return (num1<0) ^ (num2<0);
    return (num1^num2) < 0;
}

大小写字母的转换

大写字母十进制二进制小写写字母十进制二进制
A650100 0001a970110 0001
B660100 0010b980110 0010
C670100 0011c990110 0011

观察可得,大小写字母在二进制中只有第6位不同,也就是差32的理由

大小写字母互换

ch ^= 32;

统一大写

ch &= -33;
ch &= 95;

统一小写

ch |= 32;

格雷码(Gray Code)

百度百科:格雷码_百度百科 (baidu.com)

在一组数的编码中,若任意两个相邻的代码只有一位二进制数不同,且最大数与最小数之间也仅一位数不同

在电路中便于电位的跳变

力扣:89. 格雷编码

对称法

后4个由前四个轴对称构成

数值位一样,首位补1

下标实际数值3位典型格雷码
00000
11001
23011
32010
46110
57111
65101
74100
class Solution {
public:
    vector<int> grayCode(int n) {
        vector<int>ans;
        // 预留空间
        ans.reserve(1<<n);
        ans.push_back(0);

        for (int floor = 1; floor <= n; floor++) {
            int total = ans.size();
            int addOne = 1<<(floor-1);
            for (int idx = total-1; idx >= 0; idx--) {
                ans.push_back( ans[idx] | addOne );
            }
        }

        return ans;
    }
};

位运算 公式法

res[i] = i ^ (i>>1)

下标下标的二进制实际数值3位典型格雷码
00000000
10011001
20103011
30112010
41006110
51017111
61105101
71114100
class Solution {
public:
    vector<int> grayCode(int n) {
        vector<int> ret(1 << n);
        for (int i = 0; i < ret.size(); i++) {
            ret[i] = (i >> 1) ^ i;
        }
        return ret;
    }
};

二进制枚举

力扣:1601. 最多可达成的换楼请求数目

力扣:2044. 统计按位或能得到最大值的子集数目

适合数据范围较小的,类似01背包每个元素取或不取的题型

当然,由于数据量较小,直接dfs也行

class Solution {
public:
    int countMaxOrSubsets(vector<int>& nums) {
        int n = nums.size();
        int maxx = 1<<n;

        int ans = 0;
        int maxMask = 0;
        for (int mask = 1; mask <= maxx; mask++) {
            int cur = mask;
            int curMask = 0;
            for (int i = 0; i < n; i++) {
                if (cur&(1<<i)) {
                    curMask |= nums[i];
                }
            }
            if (curMask > maxMask) {
                maxMask = curMask;
                ans = 1;
            } else if (curMask == maxMask) {
                ans++;
            }
        }

        return ans;
    }
};

倍增

ST表

练习题:

洛谷:P3865 【模板】ST 表 - 洛谷

/**
 * https://www.luogu.com.cn/problem/P3865
 * P3865 【模板】ST 表
 * 倍增算法
 */
#include <bits/stdc++.h>
using namespace std;

constexpr int M = 10 + 100000;
int arr[M];     // 输入的数据
int ST[M][21];  // 存储最值

void getST(int n) {
    // 本题最小是0,且后续计算长度
    // 且单组数据,不初始化也行
    // memset(ST, -1, sizeof(ST));

    // idx开始长度为2^0=1的最值
    // 即[i, i] 是arr[i]本身
    for (int i = 1; i <= n; i++) {
        ST[i][0] = arr[i];
    }

    // 先遍历位数
    for (int j = 1; j <= 20; j++) {
        // 再遍历每个数 idx+len-1<=n
        for (int i = 1; i + (1 << j) - 1 <= n; i++) {
            // 两个j-1贡献给j
            // 计算左右起点,和RMQ的query()一样
            // mid的两侧为 (i+(1<<(j-1))-1) 和 (i+(1<<(j-1)))
            ST[i][j] = max(ST[i][j - 1], ST[i + (1 << (j - 1))][j - 1]);
        }
    }
}

int RMQ(int left, int right) {
    int k = log2(right - left + 1);
    // 右侧起点是 right + len + 1
    return max(ST[left][k], ST[right - (1 << k) + 1][k]);
}

int main() {
    int n, m;
    scanf("%d %d", &n, &m);

    for (int i = 1; i <= n; i++) {
        scanf("%d", &arr[i]);
    }

    getST(n);

    for (int i = 1, left, right; i <= m; i++) {
        scanf("%d %d", &left, &right);
        printf("%d\n", RMQ(left, right));
    }

    return 0;
}

树上倍增

练习题:

力扣:1483. 树节点的第 K 个祖先

class TreeAncestor {
private:
    vector<vector<int>> graph;
    vector<int> deep;
    vector<vector<int>> father;
    int log;

public:
    TreeAncestor(int n, vector<int>& parent) {
        this->log = log2(n) + 1;
        this->graph.resize(n + 1);
        this->deep.resize(n + 1);
        this->father.resize(n + 1, vector<int>(log + 1));

        // [0, n-1] -> [1, n]
        for (int i = 0; i < n; i++) {
            int from = parent[i] + 1;
            int to = i + 1;
            graph[from].emplace_back(to);
        }

        // root = 1
        dfs(1, 0);
    }
    
    int getKthAncestor(int node, int k) {
        node += 1;
        for (int i = log; i >= 0; i--) {
            int step = (int)pow(2, i);
            if (k && step <= k) {
                k -= step;
                node = father[node][i];
            }
        }
        return node == 0 ? -1 : node - 1;
    }

protected:
    void dfs(int cur, int from) {
        deep[cur] = deep[from] + 1;
        father[cur][0] = from;  // 2^0=1, 即直接的上一个点

        for (int i = 1; i <= log; i++) {
            father[cur][i] = father[(father[cur][i-1])][i-1];
        }

        for (int& nex : graph[cur]) {
            // 有向树其实不用判,这里模板写一下
            if (nex == from) {
                continue;
            }
            dfs(nex, cur);
        }
    }
};

双指针

获得所有字串

**描述:**给定一个字符串,获得所有字串

其实就是O(n^2)的暴力枚举

vector<string> GetAllSubstr(string &s) {
    int n = s.size();
    vector<string>ans;
    for (int i = 0; i < n; i++) {
        for (int j = i; j < n; j++) {
            string c = s.substr(i, j-i+1);
            ans.push_back(c);
        }
    }
    return ans;
}

滑动窗口

(双指针) 滑动窗口 定长与不定长_CUBE_lotus的博客-CSDN博客

定长窗口:

力扣:567. 字符串的排列

力扣:219. 存在重复元素 II

力扣:239. 滑动窗口最大值

不定长窗口:

牛客:智乃的密码

快慢指针

练习题:

力扣:283. 移动零

力扣:905. 按奇偶排序数组

// 905. 按奇偶排序数组 偶数放前,奇数放后
// 题解中还有一些别的思路,可以看一下
class Solution {
public:
    vector<int> sortArrayByParity(vector<int>& nums) {
        int n = nums.size();
        for (int i = 0, j = 0; i < n; i++) {
            if (nums[i]%2 == 0) {
                swap(nums[i], nums[j++]);
            }
        }
        return nums;
    }
};

分治

快速幂

求a^n mod p

注意: ( a ∗ b ) mod c = ( ( a m o d c ) ∗ ( b m o d c ) ) m o d c (a*b) \textit{mod} c = ((a mod c)*(b mod c)) mod c (ab)modc=((amodc)(bmodc))modc(加法乘法同理)

**原理:**根据a的二进制代码进行分治和暂存(O(logn))

P1226 【模板】快速幂||取余运算

力扣: 372. 超级次方 (该题配合了一些幂运算的性质)

递归写法

int binPow(int base, int expo, int p) {
    if (expo == 0) {    // 防止 mod 1
        return 1%p;
    }
    base %= p;
    int half = binPow(base, expo>>1, p);
    if (expo & 1) {
        return half*half%p * base%p;
    } else {
        return half*half%p;
    }
}

非递归

auto binPow = [](int base, int expo, int mod) -> int {
    int ans = 1;
    base %= mod;
    if (expo == 0) {
        ans = 1%mod;
    } else {
        while(expo) {
            if (expo & 1) {
                ans = ans * base % mod;
            }
            base = base * base % mod;
            expo >>= 1;
        }
    }
    return ans;
};

矩阵快速幂

练习题:

PTA:题目详情 - L1-048 矩阵A乘以B (pintia.cn)

洛谷:P3390 【模板】矩阵快速幂

力扣:1220. 统计元音字母序列的数目

牛客:敢敢单单的斐波那契数列 (nowcoder.com)

洛谷:P1962 斐波那契数列 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)


斐波那契数列

斐波那契的N种实现方式_天赐细莲的博客-CSDN博客

#include <bits/stdc++.h>
#define int long long
using namespace std;

const int mod = 1e9 + 7;
// 矩阵乘法
vector<vector<int>> matrixMultiply(const vector<vector<int>>& matrixA, const vector<vector<int>>& matrixB) {
    int n = matrixA.size();
    int m = matrixA[0].size();
    int p = matrixB[0].size();
    vector<vector<int>> ans(n, vector<int>(p));
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            for (int k = 0; k < p; k++) {
                ans[i][k] = (ans[i][k] + matrixA[i][j] * matrixB[j][k]) % mod;
            }
        }
    }
    return ans;
}
// 快速幂,此处不考虑0次幂的情况
vector<vector<int>> matrixBinPow(const vector<vector<int>>& matrix, int k) {
    if (k == 1) {
        return matrix;
    }

    auto ans = matrixBinPow(matrix, k>>1);
    ans = matrixMultiply(ans, ans);
    if (k & 1) {
        return matrixMultiply(ans, matrix);
    } else {
        return ans;
    }
}

signed main () {
    int n;
    cin >> n ;

    // 起始的先特判
    if (n <= 1) {
        cout << n%mod << endl;
        return 0;
    }

    // 矩阵迭代次数
    n -= 1;
    // 矩阵右侧向量
    // 这里一定要注意逻辑顺序
    vector<int> fib = {1, 0};
    // 核心
    vector<vector<int>> matrix = {
        {1, 1},
        {1, 0}
    };
    matrix = matrixBinPow(matrix, n);

    n = fib.size();
    vector<int> ans(n);
    // 其实只要算ans[0]即可
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            // 计算时注意定义的顺序
            ans[i] = (ans[i] + matrix[i][j] * fib[j]) % mod;
        }
    }

    cout << ans[0] << endl;

    return 0;
}

斐波那契矩阵递推公式

f ( n ) = f ( n − 1 ) + f ( n − 2 ) {f(n) = f(n-1) + f(n-2)} f(n)=f(n1)+f(n2)

[ f ( n ) f ( n − 1 ) ] = [ 1 ∗ f ( n − 1 ) + 1 ∗ f ( n − 2 ) 1 ∗ f ( n − 1 ) + 0 ∗ f ( n − 2 ) ] = [ 1 1 1 0 ] ∗ [ f ( n − 1 ) f ( n − 2 ) ] \begin{bmatrix} f(n) \\ f(n-1) \end{bmatrix} = \begin{bmatrix}1 * f(n-1) + 1 * f(n-2)\\ 1 * f(n-1) + 0 * f(n-2)\end{bmatrix} = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix} * \begin{bmatrix} f(n-1) \\ f(n-2) \end{bmatrix} [f(n)f(n1)]=[1f(n1)+1f(n2)1f(n1)+0f(n2)]=[1110][f(n1)f(n2)]

回归到最初的起始条件
[ f ( n ) f ( n − 1 ) ] = [ 1 1 1 0 ] ( n − 1 ) ∗ [ f ( 1 ) f ( 0 ) ] \begin{bmatrix} f(n) \\ f(n-1) \end{bmatrix} = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^{(n-1)} * \begin{bmatrix} f(1) \\ f(0) \end{bmatrix} [f(n)f(n1)]=[1110](n1)[f(1)f(0)]

测试点AC1AC2AC3AC4AC5AC6AC7AC8AC9AC10平均
ijk时间/ms5212563575913133161152.00
内存/KB608604628572760824472468740640631.60
ikj时间/ms3787454042923212836.80
内存/KB600620744756752628472624492640632.80

除法取模

费马小定理

/**
* molecular 分子
* denominator 分母
*/
int subMod(int molecular, int denominator, int mod) {
    int inverseElement = binPow(denominator, mod-2, mod);
    return (molecular * inverseElement) % mod;
}

字符串取特定字串

基本步骤

  1. 根据题意找出串中不符合条件的点(下标)
  2. 遍历点集(下标)(这就是分界点)
    1. 将串从该点分割左右两个字串
    2. 递归搜索两个字串

练习题:

力扣:1763. 最长的美好子字符串

力扣:395. 至少有 K 个重复字符的最长子串

// 395. 至少有 K 个重复字符的最长子串
class Solution {
public:
    int longestSubstring(string s, int k) {
        int n = s.size();

        // 找出当前串中字符数 < k 的下标作为分界点
        unordered_map<char, int> ump;
        for (int i = 0; i < n; i++) {
            ump[s[i]]++;
        }
        vector<int> idxSet;
        for (int i = 0; i < n; i++) {
            if (ump[s[i]] < k){
                idxSet.push_back(i); 
            }
        }

        for (int& i : idxSet) {
            string leftString = s.substr(0, i);
            string rightString = s.substr(i+1);
            int leftMaxLen = longestSubstring(leftString, k);
            int rightMaxLen = longestSubstring(rightString, k);
            return leftMaxLen >= rightMaxLen ? leftMaxLen : rightMaxLen;
        }

        return s.size();
    }
};

搜索 (BFS)

双向BFS

力扣:773. 滑动谜题

**描述:**以地图问题为例,如果已知起点和终点。

则可以设置两个队列,分别从起点,终点开始一起搜索。

如果相遇,则是找到。无法相遇,表示找不到。

该模板(伪代码)链接:双向BFS模板_SPIDER的博客-CSDN博客_双向bfs

void BFS_bothsides() { //双向BFS
    if(s1.state==s2.state) { //起点终点相同时要特判
        //do something
        found=true;
        return;
    }
    bool found=false;
    memset(visited,0,sizeof(visited));  // 判重数组
    while(!Q1.empty())  Q1.pop();   // 正向队列
    while(!Q2.empty())  Q2.pop();  // 反向队列
    //======正向扩展的状态标记为1,反向扩展标记为2
    visited[s1.state]=1;   // 初始状态标记为1
    visited[s2.state]=2;   // 结束状态标记为2
    Q1.push(s1);  // 初始状态入正向队列
    Q2.push(s2);  // 结束状态入反向队列
    while(!Q1.empty() || !Q2.empty()) {
        if(!Q1.empty())
            BFS_expand(Q1,true);  // 在正向队列中搜索
        if(found)  // 搜索结束
            return ;
        if(!Q2.empty())
            BFS_expand(Q2,false);  // 在反向队列中搜索
        if(found) // 搜索结束
            return ;
    }
}
void BFS_expand(queue<Status> &Q,bool flag) {
    s=Q.front();  // 从队列中得到头结点s
    Q.pop()
    for( 每个s 的子节点 t ) {
        t.state=Gethash(t.temp);  // 获取子节点的状态
        if(flag) { // 在正向队列中判断
            if(visited[t.state]!=1) { // 没在正向队列出现过
                if(visited[t.state]==2) { // 该状态在反向队列中出现过
                    各种操作;
                    found=truereturn;
                }
                visited[t.state]=1;   // 标记为在在正向队列中
                Q.push(t);  // 入队
            }
        } else { // 在正向队列中判断
            if (visited[t.state]!=2) { // 没在反向队列出现过
                if(visited[t.state]==1) { // 该状态在正向向队列中出现过
                    各种操作;
                    found=truereturn;
                }
                visited[t.state]=2;  // 标记为在反向队列中
                Q.push(t);  // 入队
            }
        }
    }
}

A*

原理思路:

通过构造启发式函数,用优先队列代替普通队列,在每次BFS取出点的时候,取的是计算出来的预计最优解

构造方式很多

通常在地图中可以直接算曼哈顿距离

可以通过当前步数 + 预计的最优距离的混合计算

等等

教学视频:

A*寻路算法详解 #A星 #启发式搜索_哔哩哔哩_bilibili

练习题:

力扣:675. 为高尔夫比赛砍树

class Solution {
private:
    const vector<vector<int>> MOVE = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
    vector<vector<int>> grap;
    int row, col;

    inline bool check(int x, int y) {
        return x >= 0 && x < row && y >= 0 && y < col;
    }

    int bfs(pair<int, int> start, pair<int, int> end) {
        struct Node {
            int x, y, step;
        };
        int startx = start.first, starty = start.second;
        int endx = end.first, endy = end.second;
        vector<vector<bool>> vis(row, vector<bool>(col));

        // 实际步数 + 预计的理想曼哈顿距离
        auto cmp = [&](Node& a, Node& b) {
            int lena = abs(a.x - endx) + abs(a.y - endy);
            int lenb = abs(b.x - endx) + abs(b.y - endy);
            // 大顶堆 大顶堆 大顶堆
            // 先比较启发式函数的值,再比较实际步数
            return a.step + lena != b.step + lenb
                    ? a.step + lena > b.step + lenb
                    : a.step > b.step;
        };

        priority_queue<Node, vector<Node>, decltype(cmp)> heap(cmp);
        heap.push(Node{startx, starty, 0});
        vis[startx][starty] = true;

        while (!heap.empty()) {
            int x = heap.top().x, y = heap.top().y, step = heap.top().step;
            heap.pop();

            if (x == endx && y == endy) {
                return step;
            }

            for (const vector<int>& mov : MOVE) {
                int xx = x + mov[0];
                int yy = y + mov[1];
                if (!check(xx, yy) || vis[xx][yy] || grap[xx][yy] == 0) {
                    continue;
                }
                vis[xx][yy] = true;
                heap.push(Node{xx, yy, step + 1});
            }
        }

        return -1;
    }

public:
    int cutOffTree(vector<vector<int>>& forest) {
        this->grap = forest;
        this->row = forest.size();
        this->col = forest[0].size();

        vector<int> points;
        unordered_map<int, pair<int, int>> ump;

        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (grap[i][j] > 1) {
                    points.push_back(grap[i][j]);
                    ump[grap[i][j]] = pair<int, int>{i, j};
                }
            }
        }
        sort(points.begin(), points.end());

        int ans = 0;
        pair<int, int> cur{0, 0};
        for (int i = 0; i < points.size(); i++) {
            int step = bfs(cur, ump[points[i]]);
            if (step == -1) {
                ans = -1;
                break;
            }
            ans += step;
            cur = ump[points[i]];
        }

        return ans;
    }
};

多源BFS

练习题:

力扣:1765. 地图中的最高点

天梯赛:L2-016 愿天下有情人都是失散多年的兄妹 (25 分)

class Solution {
private:
    int row ,col;
    const vector<vector<int>>MOV = { {1,0},{-1,0},{0,1},{0,-1} };

    inline bool check(int x, int y) {
        return x >= 0 && x < row && y >= 0 && y < col;
    }
public:
    vector<vector<int>> highestPeak(vector<vector<int>>& isWater) {
        this->row = isWater.size();
        this->col = isWater[0].size();

        vector<vector<bool>> vis(row, vector<bool>(col)); 
        queue<pair<int,int>> q;
        for (int i = 0 ; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (isWater[i][j] == 1) {
                    q.push({i, j});
                    vis[i][j] = true;
                }
            }
        }

        vector<vector<int>> grap(row, vector<int>(col));
        
        int step = 0;
        while (!q.empty()) {
            step++;

            int len = q.size();
            while (len--) {
                auto [x, y] = q.front();
                q.pop();
             
                for (auto mov : MOV) {
                    int xx = x + mov[0];
                    int yy = y + mov[1];
                    if (!check(xx, yy)) {
                        continue;
                    }
                    if (!vis[xx][yy]) {
                        vis[xx][yy] = true;
                        grap[xx][yy] = step;
                        q.push({xx, yy});
                    }
                } 
            }
        }

        return grap;
    }
};

搜索 (DFS)

汉诺塔

B站:【学习记录】汉诺塔问题的递归和非递归算法详细讲解-计算机算法

​ A B C

起始杆 中介杆 目标杆 (初始状态)(将 n-1 个移到B上)

起始杆 目标杆 中介杆 (则最后第n个可以移C上)

中介杆 起始杆 目标杆 (类似上面的操作,把倒数第二个搞出来)

// n表示层数
void hanoi(int n, char A, char B, char C) {
    if (n) {
        hanoi(n-1, A, C, B);
        printf("%c -> %c\n", A, C);
        hanoi(n-1, B, A, C);
    }
}

全排列

next_permutation(begin(), end())

力扣:46. 全排列

class Solution {
private:
    vector<vector<int>> ans;    //答案
    vector<int> tmp;            //用来存入答案
    map<int, bool> mp;          //标记访问
    int n;                      //表示长度

    void dfs(int cnt, vector<int> & nums) {
        if (cnt == n) { //回溯条件,长度达到相等
            ans.push_back(tmp);
            return ;
        }
        //此时是用来存入tmp的第cnt个位置的元素
        //每个元素都要作为分支来判断
        for (int i = 0; i < n ; i++) {
            if(!mp[nums[i]]) {      //若未访问
                tmp[cnt] = nums[i]; //将该元素存入tmp
                mp[nums[i]] = true; //标记已访问
                dfs(cnt+1, nums);
                mp[nums[i]] = false;//回溯
            }
        }
        return ;
    }

public:
    vector<vector<int>> permute(vector<int>& nums) {
        n = nums.size();
        tmp.resize(n);
        dfs(0, nums);
        return ans;
    }
};

获得子集

力扣:78. 子集

输入:nums = [1,2,3]

输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

class Solution {
private:
    vector<vector<int>> ans;
    vector<int>tmp;
    int n;  //ans的长度

    void dfs(int cur, vector<int>& nums) {
        if (cur == n) { //因为最多存n个元素,所有return 条件是cur == n
            ans.push_back(tmp);
            return ;
        }
        //把元素搬到到tmp里,在递归dfs中push_back到ans中
        tmp.push_back(nums[cur]); //选择当前位置
        dfs(cur+1, nums);
        tmp.pop_back(); //回溯    //不选择当前位置
        dfs(cur+1, nums);
        return ;
    }
//输出结果为:
//[[1,2,3],[1,2],[1,3],[1],[2,3],[2],[3],[]]
//第一个元素是一路全放进去,第一个碰到开头的return ;
//最后一个元素是全部元素回溯后,为空,再最后碰到return ;
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        n = nums.size();    //获取长度
        dfs(0, nums);
        return ans;
    }
};

地图类回溯问题

力扣:1219. 黄金矿工

一种走法获得路径的最大值

class Solution {
private:
    const vector<vector<int>> MOV = {{1,0},{-1,0},{0,1},{0,-1}};
    vector<vector<int>> grid;
    int ans;
    int row, col;
public:
    int getMaximumGold(vector<vector<int>>& grid) {
        this->grid = grid;
        this->row = grid.size();
        this->col = grid[0].size();

        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (this->grid[i][j] != 0) {
                    dfs(i, j, this->grid[i][j]);
                }
            }
        }

        return ans;
    }

private:
    inline bool check(int x, int y) {
        return x >= 0 && x < row && y >= 0 && y < col;
    }

    void dfs(int x, int y, int cur) {
        ans = max(ans, cur);

        int tmp = grid[x][y];
        grid[x][y] = 0;

        for (auto& mov : MOV) {
            int xx = x + mov[0];
            int yy = y + mov[1];
            if (!check(xx,yy)) {
                continue;
            }
            if (grid[xx][yy] > 0) {
                dfs(xx, yy, cur+grid[xx][yy]);
            }
        }

        grid[x][y] = tmp;
    }
};

两个DFS互套

力扣:913. 猫和老鼠

基于 两个DFS 互套的 记忆化搜索的 动态规划

思维难度极大,本题的数据量有补充该代码不能AC,但很值得学习

class Solution {
private:
    const int MOUSE_WIN = 1;
    const int CAT_WIN = 2;
    const int DRAW = 0;

    int n;
    // mouse cat turns
    // turns 是猫鼠分别一步,就是两倍空间
    vector<vector<vector<int>>>dp;
    vector<vector<int>>graph;

public:
    int catMouseGame(vector<vector<int>>& graph) {
        this->n = graph.size();
        this->graph = graph;
        this->dp.resize(n+1, vector<vector<int>>(n+1, vector<int>((n+1)*2, -1)));
        // 初始状态 老鼠1,猫2,第0步
        return getResult(1, 2, 0);
    }

    int getResult(int mouse, int cat, int turns) {
        // 步数走完一轮,第二轮必然会重叠,就是平局
        if (turns == n*2) {
            return DRAW;
        }
        // -1表示未该状态未获得结果
        if (dp[mouse][cat][turns] < 0) {
            if (mouse == 0) {
                // 老鼠进洞获胜
                dp[mouse][cat][turns] = MOUSE_WIN;
            } else if (mouse == cat) {
                // 猫捉到老鼠获胜
                dp[mouse][cat][turns] = CAT_WIN;
            } else {
                // 获得下一个状态
                getNextResult(mouse, cat, turns);
            }
        }

        return dp[mouse][cat][turns];
    }

    void getNextResult(int mouse, int cat, int turns) {
        int curMove = turns&1 ? cat : mouse;
        // 如果当前主体位置是老鼠暂定猫赢,如果当前位置是猫则老鼠赢
        // 默认当前主体败,寻找胜或平局的可能
        // 预期结果
        int defaultResult = curMove == mouse ? CAT_WIN : MOUSE_WIN;
        // 真实结果
        int result = defaultResult;

        for (auto & next : graph[curMove]) {
            // 猫不可进洞
            if (curMove == cat && next == 0) {
                continue;
            }
            // 老鼠和猫的下一个状态
            int nextMOUSE = curMove == mouse ? next : mouse;
            int nextCAT = curMove == cat ? next : cat;
            // 获得下一个状态位的结果
            int nextResult = getResult(nextMOUSE, nextCAT, turns+1);
            // 与必败的预期不同
            if (nextResult != defaultResult) {
                // 修改真实结果
                result = nextResult;
                // 不是平局,就是有一方获胜,终止循环
                if (result != DRAW) {
                    break;
                }
            }
        }

        // 记忆化修改
        dp[mouse][cat][turns] = result;
    }
};

经典回溯

练习题:

力扣:698. 划分为k个相等的子集

class Solution {
   public:
    bool canPartitionKSubsets(vector<int>& nums, int k) {
        int n = nums.size();
        int sum = accumulate(nums.begin(), nums.end(), 0);
        int each = sum / k;
        if (each * k != sum) {
            return false;
        }
        // 从大到小排序帮助剪枝
        sort(nums.begin(), nums.end(), greater<>());
        if (nums.front() > each) {
            return false;
        }

        // 借助仿函数手写hash
        struct MyHash {
            const uint64_t PRIME = 13331;
            inline uint64_t operator() (const vector<int>& arr) const {
                uint64_t sum = 0;
                for (const int& num : arr) {
                    sum = sum*PRIME + num;
                }
                return sum;
            }
        };

        vector<int> cnt(k);
        unordered_map<vector<int>, bool, MyHash> vis;
        function<bool(int)> dfs = [&](int pos) -> bool {
            auto pre = cnt;
            sort(pre.begin(), pre.end());
            if (vis[pre]) {
                return false;
            }
            if (pos == nums.size()) {
                return true;
            }

            for (int i = 0; i < cnt.size(); i++) {
                cnt[i] += nums[pos];
                // 借助下一轮的结果判断当前这条路能否走通
                if (cnt[i] <= each && dfs(pos + 1)) {
                    return true;
                }
                cnt[i] -= nums[pos];
            }
            // 记忆化
            vis[pre] = true;
            return false;
        };

        return dfs(0);
    }
};

动态规划 (DP)

线性dp

斐波那契的N种实现方式_天赐细莲的博客-CSDN博客

最长递增子序列

力扣:300. 最长递增子序列

法一:动态规划

int lengthOfLIS(vector<int>& nums) {
    int n = nums.size();
    vector<int> dp(n, 1);
    for (int i = 0; i < n ; i++) {
        for (int j = 0; j < i; j++) {
            if (nums[i] > nums[j]) {
                dp[i] = max(dp[i], dp[j]+1);
            }
        } 
    }
    return *max_element(dp.begin(), dp.end());
}

法二:贪心+二分查找

评论区企业级理解:

一个新员工一个老员工价值相当,老员工就可以走了,因为新员工被榨取的剩余空间更多。

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size();
        // 规定长度是len的上升子序列的末尾是多少
        vector<int> tail;
        tail.push_back(nums[0]);

        // 从下标1开始往前dp
        for (int i = 1; i < n; ++i) {
            if (tail.back() < nums[i]) {
                //如果新元素比尾部还大,则LIS的长度+1
                tail.push_back(nums[i]);
            } else {
                // 更新序列中同等低位的最大值>=nums[i]
                int left = 0;
                int right = tail.size()-1;
                int ans = -1;
                // 二分找首个大于等于该值的
                while (left <= right) {
                    int mid = (right-left)/2 + left;
                    if (tail[mid] < nums[i]) {
                        left = mid + 1;
                    } else {
                        ans = mid;
                        right = mid - 1;
                    }
                }
                tail[ans] = nums[i];
            }
        }

        return tail.size();
    }
};

非动态规划法(本质与上面的二分法相同)

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        set<int> st;
        for (int num : nums) {
            // 根据是否要求严格递增选择
            // auto it = st.upper_bound(num);
            auto it = st.lower_bound(num);
            
            if (it != st.end()){
                // 若存在比当前大的值,则去除
                st.erase(it);
            }
            st.insert(num);
        }
        return st.size();
    }
};

拓展题:

673. 最长递增子序列的个数

1713. 得到子序列的最少操作次数

最长公共子序列

1143. 最长公共子序列

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int n = text1.size();
        int m = text2.size();
        // 添加哨兵,便于dp的边界操作
        string s = ' ' + text1;
        string t = ' ' + text2;
        // i为s的每一个字符,j为t的每一个字符
        // 第i个字符到第j个字符可以匹配的长度
        vector<vector<int>> dp(n+1, vector<int>(m+1));

        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                if (s[i] == t[j]) {
                    // 有相等的,可以从两个串一起往前一个字符+1
                    dp[i][j] = dp[i-1][j-1] + 1;
                } else {
                    // 没有相等的,要么追随当前层前一位置,要么追随上一层的该位置
                    dp[i][j] = max(dp[i][j-1], dp[i-1][j]);
                }
            }
        }
        // 末尾全匹配完,最大的dp
        return dp[n][m];
    }
};

最大子序和

力扣:53. 最大子序和

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int sum = INT_MIN/2;
        int ans = INT_MIN/2;
        for (int& num : nums) {
            sum = max(sum+num, num);
            ans = max(ans, sum);
        }
        return ans;
    }
};

二维前缀和

注意重叠部分

力扣:304. 二维区域和检索 - 矩阵不可变

class NumMatrix {
private:
    vector<vector<int>> pre;

public:
    NumMatrix(vector<vector<int>>& matrix) {
        int row = matrix.size(), col = matrix[0].size();
        this->pre.resize(row+1, vector<int>(col+1));

        for (int i = 1; i <= row; ++i) {
            for (int j = 1; j <= col; ++j) {
                pre[i][j] = pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1] + matrix[i-1][j-1];
            }
        }
    }
    // [0, n-1]
    int sumRegion(int row1, int col1, int row2, int col2) {
        row1 += 1, col1 += 1, row2 += 1, col2 += 1;
        return pre[row2][col2] - pre[row2][col1-1] - pre[row1-1][col2] + pre[row1-1][col1-1];
    }
};

/**
 * Your NumMatrix object will be instantiated and called as such:
 * NumMatrix* obj = new NumMatrix(matrix);
 * int param_1 = obj->sumRegion(row1,col1,row2,col2);
 */

经典二维堆木块

练习题:

洛谷:P1990 覆盖墙壁

蓝桥杯:积木画

题目描述:

在2*N的面积上方木块

木块有1*2 和 L型的无限个

m7vipknd.png (458×262) (luogu.com.cn)


思路:

  • 竖着放1个1*2 dp[i-1]
  • 横着放2个1*2 ``dp[i-2]
  • 放L型的,可以旋转有两种方式
    • 至少补一个L型来补充一个缺口 此时dp[i-2]
    • 在两个L之间乐意放奇数个1*2填充,每填充一个长度+1
      • 若:补充一个dp[i-4]
      • 若:补充两个dp[i-5]
    • 当然在这之间也可以补充偶数个L,与补充1*2等效
      • 因此至少是达到 dp[i-3]
  • 总结 2*(dp[i-3] + dp[i-4] + dp[i-5] + ... + dp[0])
  • 因此可以用前缀和来处理
/** 洛谷P1990 覆盖墙壁 */
#include<bits/stdc++.h>
using namespace std;

const int mod = 10000;
const int M = 10 + 1000000;

int dp[M];
int pre[M];

int main () {
    int n;
    cin >> n;

    // 什么都不放一种可能
    dp[0] = 1, dp[1] = 1, dp[2] = 2;
    // 前缀和
    pre[0] = 1, pre[1] = 2, pre[2] = 4;

    for (int i = 3; i <= n; i++) {
        dp[i] = (dp[i-1] + dp[i-2] + 2*pre[i-3]) % mod;;
        pre[i] = (pre[i-1] + dp[i]) % mod;
    }

    cout << dp[n] << endl;

    return 0;
}

背包dp

(背包dp) 背包N讲_天赐细莲的博客-CSDN博客

**问题描述:**给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。

核心思路是一个物品,有取或不取两种状态

取的化对占用一定容量,需要从小容量转化过来

视频讲解,B站:0/1背包问题-动态规划 Knapsack_problem Dynamic Programming

VW0123456
000000000
630006666
1010101010161616
520101015161621
1040101015162021

区间dp

最长回文子序列

力扣:516. 最长回文子序列

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int n = s.size();
        vector<vector<int>>dp(n, vector<int>(n));

        //单个字母可以作为一个回文串
        for (int i = 0; i < n; i++){
            dp[i][i] = 1;
        }
        //区间dp,先确定长度,此处单个字母的1已处理,可以从2开始
        for (int len = 2; len <= n; len++){
            //枚举左端点,中间的Boolean可以用右端点换元计算
            for (int left = 0; left < n-len+1; left++){
                int right = left+len-1;
                if (s[left] == s[right]){
                    //左右字符相同,则可扩展成内部夹的dp+2
                    dp[left][right] = dp[left+1][right-1]+2;
                }else{
                    //左右字符不等,则最多加一个字符,所以比较左右max
                    dp[left][right] = max(dp[left+1][right], dp[left][right-1]);
                }
            }
        }
        //返回最大区间0~n-1
        return dp[0][n-1];
    }
};

数位dp

windy数

讲解:

9.107 Windy数 数位DP

练习题:

P2657 windy 数

力扣:600. 不含连续1的非负整数

不含前导零且相邻两个数字之差至少为 2 的正整数被称为 windy 数。

windy 想知道,在 a 和 b 之间,包括 a和 b ,总共有多少个 windy 数?

#include <bits/stdc++.h>
using namespace std;

const int M = 1 + 10;
inline bool check(const int cur, const int close) {
    return abs(cur - close) >= 2;
}

// 第几位
// 0~9
// 合法的状态次数
int dp[M][10];

void init() {
    // 初始化1位数均合格
    for (int i = 0; i <= 9; i++) {
        dp[1][i] = 1;
    }

    // 枚举位数
    for (int bit = 2; bit < M; bit++) {
        // 枚举当前位
        for (int cur = 0; cur <= 9; cur++) {
            // 枚举邻近位(前一位)
            for (int pre = 0; pre <= 9; pre++) {
                if (check(cur, pre)) {
                    // 合法则累计前一位处于j的情况
                    dp[bit][cur] += dp[bit-1][pre];
                }
            }
        }
    }

    return ;
}

int getWindy(int n) {
    // 特判
    if (n == 0) {
        return 0;
    }
    // 拆分数字
    // 个位 十位 百位 。。。
    vector<int> eachBit;
    while (n) {
        int tmp = n%10;
        n /= 10;
        eachBit.push_back(tmp);
    }

    // 处理与目标数值位数相等的情况
    int res1 = 0;

    // 表示上一位数字,因题而异
    int pre = -2;
    // 逆向遍历 (高位到低位)
    for (int i = eachBit.size()-1; i >= 0; i--) {
        // 注意,第一个维度的位数有预留的0位置
        int bit = i+1;
        // 当前数字
        int cur = eachBit[i];

        // 最高位置从1开始 (无前导0)
        // 非最高位从0开始
        for (int j = (i == eachBit.size()-1); j < cur; j++) {
            if (check(j, pre)) {
                res1 += dp[bit][j];
            }
        }

        // 剪枝,不合格可直接break
        if (!check(cur, pre)) {
            break;
        }
        // 为下一轮迭代更新前驱位置
        pre = cur;
        // 特判,能走到最后,说明该数本身也是合格的
        if (i == 0) {
            res1++;
        }
    }

    // 处理低于目标值位数的情况
    // 直接累计预处理的dp数组
    int res2 = 0;

    // 表示每一个位数
    for (int bit = 1; bit <= eachBit.size()-1; bit++) {
        // 表示的是i位数,则不能有前导0
        for (int num = 1; num <= 9; num++) {
            res2 += dp[bit][num];
        }
    }

    // 相同位数+ 低位数
    return res1 + res2;
}

int main (void) {

    init();

    int left, right;
    cin >> left >> right;

    int ans = getWindy(right) - getWindy(left-1);

    cout << ans << endl;

    return 0;
}

树形dp

数塔(数字三角形)

7

3 8

8 1 0

2 7 4 4

4 5 2 6 5

[洛谷:P1216 USACO1.5][IOI1994]数字三角形 Number Triangles

//dp数组定义时要预留首末位置,便于dp时的边界操作
    memset(dp, 0, sizeof(dp));
    cin>>m;//层数
    for(int i = 1; i <= m; i++)
        for (int j = 1; j <= i; j++)
            cin>>dp[i][j];

    for (int i = m-1; i >= 1; i--)
        for (int j = 1; j <= i; j++)
            dp[i][j] += max(dp[i+1][j], dp[i+1][j+1]);

    cout<<dp[1][1]<<endl;

差分

差分往往要借助前缀和 树状数组 等方式来表现效果

线性状态累计

注意是 0~n-1 还是 1~n

这种题计算前缀到最后,往往前缀和的最后一个位置是0 因为累计的状态全部抵消了

练习题:

力扣:1109. 航班预订统计

力扣:798. 得分最高的最小轮调

class Solution {
public:
    vector<int> corpFlightBookings(vector<vector<int>>& bookings, int n) {
        // n为线性长度
        vector<int> diff(n+1);
        for (auto& arr : bookings) {
            // 原题1~n,这里转化为0~n-1
            int start = arr[0]-1;
            int end = arr[1]-1;
            int val = arr[2];
            // 起始状态叠加,终止位置的后一个消去
            diff[start] += val;
            diff[end+1] -= val;
        }

        // 求出前缀和,每个点对应底几(1~n)个位置的状态
        vector<int> pre(diff.size() + 1);
        for (int i = 0; i < diff.size(); i++) {
            pre[i+1] = pre[i] + diff[i];
        }

        // 回到原题,为了return
        // 删除为了便于差分而在n后面的添加的尾位置的后一个位置,该位置因为全部的累计抵消,必然为0
        pre.pop_back();
        // 删除求前缀和首部的预留空位置,该位置无数据,为0
        pre.erase(pre.begin());
        
        return pre;
    }
};

线性路径覆盖累计问题

牛客:C-蓝彗星_2022牛客寒假算法基础集训营4 (nowcoder.com)

题意&输入:

*第一行输入:*n 线性长度 t 每个点持续的长度

*第二行输入:*一行长度为n的只含有’B’和’R’的字符串

*第三行输入:*n个点的起始位置

1 ≤ n , t , a i ≤ 100000 1≤n,t,ai≤100000 1n,t,ai100000 数据再大需要先离散化

每个点与字符串的BR形成一一映射

*求:*该线性路径上只有B状态的长度总和

题解:

构造差分数组

  • 起始位置标记
  • 终止位置撤销标记

将差分数组构造前缀和

(前缀和的作用就是累计之前的状态)

#include <iostream>
using namespace std;

const int M = 10 + 100000;

int blue[M*2];
int red[M*2];

int main() {

    int n, t;
    cin >> n >> t;

    string s;
    cin >> s;

    for (int i = 0; i < n; i++) {
        int start;
        cin >> start;
        // 差分,开始累计+1,结束累计-1
        if (s[i] == 'B') {
            blue[start] += 1;
            blue[start+t] -= 1;
        } else {
            red[start] += 1;
            red[start+t] -= 1;
        }
    }

    int ans = 0;
    for (int i = 1; i <= M*2; i++) {
        blue[i] += blue[i-1];
        red[i] += red[i-1];

        ans += (blue[i] != 0) && (red[i] == 0);
    }

    cout << ans << endl;

    return 0;
}

有序集合

可以用差分做,但数据范围很大,且有动态更新,动态查询

当然也可以用线段树做

练习题:

力扣:699. 掉落的方块

力扣:732. 我的日程安排表 III 每次在区间[start, end-1]+1,并返回整个区间的最大值

力扣:715. Range 模块 纯模板 区间覆盖型

// 715. Range 模块
class RangeModule {
private:
    // 表示从key开始到nextKey之间的状态[左闭右开) (关键!!!)
    map<int, bool> orderedMap;
public:
    RangeModule() {
        // 类似于dp的初始状态
        // 表示:0到∞处的状态 [0, +∞)
        // 同时也避免了迭代器一直和end()判断
        orderedMap[0] = false;
    }

    void addRange(int left, int right) {
        // 添加标记为ture
        change(left, right - 1, true);
    }

    // 本题 [left, right)
    bool queryRange(int left, int right) {
        right += -1;
        auto leftIt = orderedMap.upper_bound(left),
             rightIt = orderedMap.upper_bound(right);

        // 遍历 [left, right]之间的所有状态
        // left在prev(leftIt)的区域
        for (auto it = prev(leftIt); it != rightIt; ++it) {
            if (it->second == false) {
                return false;
            }
        }

        return true;
    }

    void removeRange(int left, int right) {
        // 删除标记为 false
        change(left, right - 1, false);
    }
protected:
    // [left, right]
    void change(int left, int right, bool state) {
        // upper_bound 严格大于
        auto leftIt = orderedMap.upper_bound(left),
             rightIt = orderedMap.upper_bound(right);
        // 右端点后面的状态
        bool overRight = prev(rightIt)->second;

        // 覆盖型,因此先把中间的状态删除
        orderedMap.erase(leftIt, rightIt);

        // 标记左端点代表的状态
        orderedMap[left] = state;
        // 若右端点+1存在则保留,否则赋前面记录的值
        if (orderedMap.find(right + 1) == orderedMap.end()) {
            orderedMap[right + 1] = overRight;
        }
    }
};

链表

std::list 的运用

list<char> lst;
list<char>::iterator cur;

// insert 在 pos `前`插入 value
lst.insert(cur, data);

// 到前一个目标位置
// 返回删除目标的后一个位置
cur = lst.erase(cur);

单向链表反转

力扣:206. 反转链表

/* Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* reverseList(struct ListNode* head) {
    // pre直接等于NULL最后就不用再处理尾部
    struct ListNode* pre = NULL;
    struct ListNode* cur = head;
    while (cur != NULL) {
        struct ListNode* nex = cur->next;
        cur->next = pre;  //当前改变指向
        pre = cur;        //前驱后移
        cur = nex;        //当前后移
    }
    //跳出循环的时候,cur == NULL
    //但pre还是前驱,正好是(反转后的)第一个节点
    return pre;
}

双向链表运用

练习题:

力扣:6093. 设计一个文本编辑器

class TextEditor {
private:
    list<char> lst;
    // 始终指向 目标字符的后一个位置
    list<char>::iterator cur;

public:
    TextEditor() { cur = lst.begin(); }

    void addText(string s) {
        // insert 在 pos `前`插入 value
        for (int i = 0; i < s.size(); i++) {
            lst.insert(cur, s[i]);
        }
    }

    int deleteText(int k) {
        int ans = 0;
        while (k-- && cur != lst.begin()) {
            // 到前一个目标位置
            // 返回删除目标的后一个位置
            cur = lst.erase(prev(cur));
            ans++;
        }
        return ans;
    }

    string cursorLeft(int k) {
        while (k-- && cur != lst.begin()) {
            cur = prev(cur);
        }
        return sub10str();
    }

    string cursorRight(int k) {
        while (k-- && cur != lst.end()) {
            cur = next(cur);
        }
        return sub10str();
    }

protected:
    string sub10str() {
        string s;
        // 注意这里指的是目标字符的后一个位置
        auto it = cur;
        for (int i = 0; i < 10 && it != lst.begin(); i++) {
            // 先转到指向目标字符位置
            it = prev(it);
            s += *it;
        }
        reverse(s.begin(), s.end());
        return s;
    }
};

约瑟夫环问题 (Josephe)

练习题:1823. 找出游戏的获胜者

循环链表

class Solution {
private:
    struct Node {
        int val;
        Node* next;
    };

public:
    int findTheWinner(int n, int k) {
        // 构建循环链表
        Node * head = new Node({1, nullptr});
        Node * cur = head;
        for (int i = 2; i <= n; ++i) {
            cur->next = new Node({i, nullptr});
            cur = cur->next;
        }
        cur->next = head;

        // 初始的前驱是队尾
        Node * pre = cur;
        cur = head;
        while (cur->next != cur) {
            for (int i = 1; i < k; ++i) {
                pre = cur;
                cur = cur->next;
            }
            pre->next = pre->next->next;
            delete cur;
            cur = pre->next;
        }

        int ans = cur->val;
        delete cur;
        return ans;
    }
};

顺序表

class Solution {
public:
    int findTheWinner(int n, int k) {
        vector<int> arr(n);
        iota(arr.begin(), arr.end(), 1);

        int pos = 0;
        while (n != 1) {
            // 直接跳到删除位置
            pos += k-1;
            // 循环结构
            pos %= n;
            // 维护一个连续的顺序表
            for (int i = pos; i < n-1; ++i) {
                arr[i] = arr[i+1];
            }
            // 每删除一个长度减一
            n -= 1;
        }

        return arr.front();
    }
};

递归

class Solution {
public:
    int findTheWinner(int n, int k) {
        // 题意是从1~n
        if (n <= 1) {
            return 1;
        }
        // 前一轮的结果再走k步
        int ans = (findTheWinner(n-1, k) + k) % n;
        // 到最后位置因为是取余操作所以会回到0
        return ans == 0 ? n : ans;
    }
};

dp 迭代

由递归思路转化而来

class Solution {
public:
    int findTheWinner(int n, int k) {
        int dp = 0;
        for (int i = 2; i <= n; i++) {
            dp = (dp + k) % i;
        }
        return dp+1;
    }
};

栈与队列

单调栈

练习题:

力扣:496. 下一个更大元素 I

class Solution {
public:
    vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
        int len1 = nums1.size();
        int len2 = nums2.size();
        unordered_map<int, int> ump;

        // 该栈存储下标
        stack<int> stk;
        for (int i = 0; i < len2; i++) {
            // 非空,且当前值比栈顶的大
            // 则该值就是栈顶元素的下一个更大值
            while (!stk.empty() && nums2[i] > nums2[stk.top()]) {
                int num = nums2[stk.top()];
                ump[num] = nums2[i];
                stk.pop();
            }
            stk.push(i);
        }
        // 无法出栈就是找不到下一个更大值,设为-1
        while (!stk.empty()) {
            int num = nums2[stk.top()];
            ump[num] = -1;
            stk.pop();
        }

        vector<int> ans(len1);
        for (int i = 0; i < len1; i++) {
            int num = nums1[i];
            ans[i] = ump[num];
        }
        return ans;
    }
};

队列

单调队列

通常用双端队列,这样两头都能维护,且能取最值

注意:追加的时候,通常只能在一端追加

练习题:

力扣:239. 滑动窗口最大值 经典例题

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        int n = nums.size();
        vector<int> ans;

        // 单调队列
        // 本题维护单调递减,队首即为答案
        deque<int> dq;
        for (int i = 0; i < k; i++) {
            int cur = nums[i];
            // 追加时,从后往前看
            while (!dq.empty() && cur >= nums[dq.back()]) {
                dq.pop_back();
            }
            // 一直保持单调递减的状态
            // 则此时队首即为最大值
            dq.push_back(i);
        }
        ans.push_back(nums[dq.front()]);

        for (int i = k; i < n; i++) {
            int cur = nums[i];
            // 从后往前追加
            while (!dq.empty() && cur >= nums[dq.back()]) {
                dq.pop_back();
            }
            dq.push_back(i);  // 下方循环不用担心队列为空
            // 判断前方的元素是否是超出窗口范围
            // 因为是从后追加的,因此越前方的元素越远离
            while (dq.front() < i - k + 1) {
                dq.pop_front();
            }
            // 循环结束,此时队列中所有元素单减
            // 且所有元素在当前合法窗口内

            // 单调减,队首最大
            ans.push_back(nums[dq.front()]);
        }

        return ans;
    }
};

实现 split(string, char)

// delimitation n. 划分
function<vector<string>(string, char)> split = [](const string& s, char delim) -> vector<string> {
    vector<string> ans;
    string cur;
    for (char ch : s) {
        if (ch != delim) {
            cur += ch;
        } else if (cur.size() > 0) {
            ans.push_back(move(cur));
            cur.clear(); // move将原对象的数据转移后,原对象为空
        }
    }
    if (cur.size() > 0) {
        ans.push_back(move(cur));
    }
    return ans;
};

KMP

视频讲解:

182 KMP 算法_哔哩哔哩_bilibili

练习题:

力扣:28. 实现 strStr()

洛谷:P3375 【模板】KMP字符串匹配

不可覆盖字串数量

杭电:剪花布条 - 2087

// [P3375 【模板】KMP字符串匹配](https://www.luogu.com.cn/problem/P3375)
#include <bits/stdc++.h>
using namespace std;

int main() {
    string s, t;
    cin >> s >> t;
    // 添加哨兵节点,使串头从1开始
    s = ' ' + s;
    t = ' ' + t;
    // 模式串从第1个元素开始
    // 匹配串从前一个位置开始,预判下一个

    vector<int> next(t.size());
    next[1] = 0;  // next1必然为0
    // next数组起步要进行一步的偏移
    for (int i = 2, j = 0; i < t.size(); i++) {
        while (j > 0 && t[i] != t[j + 1]) {
            j = next[j];
        }
        if (t[i] == t[j + 1]) {
            j++;
        }
        next[i] = j;
    }

    vector<int> index;
    // 这里从s[1]开始,避免下方的s[i+1]可能出现的越界死循环
    for (int i = 1, j = 0; i < s.size(); i++) {
        // 采取预判下一个的方法
        // 直到j回到0(哨兵) 或者 下一个匹配
        while (j > 0 && s[i] != t[j + 1]) {
            j = next[j];
        }
        // 匹配j下一个位置则走一步
        if (s[i] == t[j + 1]) {
            j++;
        }
        // j到末尾,匹配完成
        if (j + 1 == t.size()) {
            // 根据题意从0\1开始
            index.push_back(i - j + 1);
            // 进行下一轮匹配
            // 不能重叠则 j = 0
            // 能重叠则j = next[j]
            j = next[j];
        }
    }
    
    // 匹配的位置
    for (int& idx : index) {
        cout << idx << endl;
    }
    // 根据题意,next数组不输出哨兵
    for (int i = 1; i < next.size(); i++) {
        cout << next[i] << " \n"[i + 1 == next.size()];
    }

    return 0;
}

Rabin-Karp 字符串哈希

hash 哈希值

prime 素数

len 串长

ch(-‘a’) 字符值(不减’a‘也行)

∑ i = 0 l e n − 1 s [ i ] ∗ p r i m e l e n − i − 1 \sum_{i = 0}^{len-1} s[i] * prime^{len-i-1} i=0len1s[i]primeleni1

h a s h = s [ 0 ] ∗ p r i m e l e n − 1 + s [ 1 ] ∗ p r i m e l e n − 2 + ⋅ ⋅ ⋅ + s [ l e n − 1 ] ∗ p r i m e 0 hash = s[0] * prime^{len-1} + s[1] * prime^{len-2} + ··· + s[len-1] * prime^0 hash=s[0]primelen1+s[1]primelen2+⋅⋅⋅+s[len1]prime0

讲解:

[【微扰理论】Rabin-Karp + 二分搜索]([微扰理论]Rabin-Karp + 二分搜索 - 最长重复子串 - 力扣(LeetCode) (leetcode-cn.com))

181 字符串哈希_董晓算法

uint64_t 就是 unsigned long long 会自动取模

练习题:

力扣:187. 重复的DNA序列

力扣:1044. 最长重复子串

洛谷:P3370 【模板】字符串哈希

class RabinKarp {
private:
    static const uint64_t PRIME = 13331;

    string s;

    vector<uint64_t> hash;
    vector<uint64_t> power;

protected:
    void init() {
        hash.resize(s.size());
        power.resize(s.size());
        hash[0] = 0;
        power[0] = 1;
        for (int i = 1; i < s.size(); i++) {
            power[i] = power[i - 1] * PRIME;
            hash[i] = hash[i - 1] * PRIME + s[i];
        }
    }

public:
    RabinKarp(const string& s) {
        this->s = ' ' + s;
        init();
    }

    uint64_t getStrHash() { return hash.back(); }

    uint64_t getSubstrHash(int left, int right) {
        left += 1, right += 1;
        return hash[right] - hash[left - 1] * power[right - left + 1];
    }
};

class Solution {
private:
    static const int L = 10;

public:
    vector<string> findRepeatedDnaSequences(string s) {
        unique_ptr<RabinKarp> unptr(new RabinKarp(s));

        vector<string> ans;
        unordered_map<uint64_t, int> ump;
        for (int left = 0, right = 0 + L - 1; right < s.size(); left++, right++) {
            auto hash = unptr->getSubstrHash(left, right);
            if (ump[hash] == 1) {
                ans.push_back(s.substr(left, right - left + 1));
            }
            ++ump[hash];
        }
        return ans;
    }
};

最小表示法

在循环同构串中,寻找最小字典序的串

方法:破环成链,用双指针在链上进行比较

练习题:

洛谷:P1368 【模板】最小表示法

力扣:899. 有序队列

力扣:796. 旋转字符串

class Solution {
public:
    string orderlyQueue(string s, int k) {
        if (k >= 2) {	// 本题另一个难点,模板请无视
            sort(s.begin(), s.end());
        }
        int idx = minimalNotation(s);
        return s.substr(idx) + s.substr(0, idx);
    }

    // 最小表示法,获得最小字典序的位置
    int minimalNotation(const string& t) {
        int n = t.size();
        // 破环成链
        string s = t + t;
        // 错开一个位置开始比较
        int i = 0, j = 1;
        while (i < n && j < n) {
            int k = 0;  // 计算相等的距离
            for (k = 0; k < n && s[i+k] == s[j+k]; k++) ;
            // 字典序大的一边跳跃
            s[i+k] > s[j+k] ? i = i+k+1 : j = j+k+1;
            // 若位置相同则任意一个指针+1
            if (i == j) {
                j++;
            }
        }
        return min(i, j);
    }
};

树状数组

专题:

(树) 树状数组_天赐细莲的博客-CSDN博客

线段树 SegmentTree

专题:

(线段树) 基础线段树常见问题总结_天赐细莲的博客-CSDN博客

字典树

练习题:

力扣:208. 实现 Trie (前缀树)

力扣:676. 实现一个魔法字典

class Trie {
private:
    vector<Trie*>children;
    bool isEnd;

    Trie * searchPrefix(string prefix) {
        Trie * node = this;
        for (auto ch : prefix) {
            ch -= 'a';
            if (node->children[ch] == nullptr)
                return nullptr;
            node = node->children[ch];
        }
        return node;
    }

public:
    /** Initialize your data structure here. */
    Trie():children(26), isEnd(false) {}

    /** Inserts a word into the trie. */
    void insert(string word) {
        Trie * node = this; //可以理解为根结点的指针
        for (char ch : word) {
            ch -= 'a';  //把字母转换成下标
            if (node->children[ch] == nullptr)  //找不到,增加字母
                node->children[ch] = new Trie();
            node = node->children[ch];
        }
        node->isEnd = true; //标记结束
    }

    /** Returns if the word is in the trie. */
    //搜索单词
    bool search(string word) {
        Trie * node = this->searchPrefix(word);
        //存在这个结点,并且有合法的终止
        return node != nullptr && node->isEnd;
    }

    /** Returns if there is any word in the trie that starts with the given prefix. */
    //搜索单词
    bool startsWith(string prefix) {
        return this->searchPrefix(prefix) != nullptr;
    }
};

树链剖分

经典应用,树上修改 树链剖分 + 线段树

洛谷:P3384 【模板】轻重链剖分/树链剖分

区间加值 区间求和

/**
 * P3384 【模板】轻重链剖分树链剖分
 * https://www.luogu.com.cn/problem/P3384
 */
#include <bits/stdc++.h>
#define int long long
using namespace std;

#define ls (root << 1)
#define rs (root << 1 | 1)

const int M = 10 + 1 * 100000;
static int mod = 1e9 + 7;  // 不用const 手动输入
int n;                     // 数据范围
/** ******************************************************************/

vector<int> oldVal(M);  // 点权的初始值
vector<int> newVal(M);  // 剖分后对应的值
/** ******************************************************************/

// 线段树模板
struct SegTreeNode {
    int val;
    int lazy;
};
vector<SegTreeNode> segTree(M << 2);

void pushUp(int root) {
    segTree[root].val = (segTree[ls].val + segTree[rs].val) % mod;
}

void pushDown(int root, int left, int right) {
    int mid = left + (right - left) / 2;
    int leftLen = mid - left + 1;
    int rightLen = right - mid;
    if (segTree[root].lazy != 0) {
        // val 累计lazy*len
        segTree[ls].val =
            (segTree[ls].val + segTree[root].lazy * leftLen) % mod;
        segTree[rs].val =
            (segTree[rs].val + segTree[root].lazy * rightLen) % mod;
        // lazy 直接累计
        segTree[ls].lazy = (segTree[ls].lazy + segTree[root].lazy) % mod;
        segTree[rs].lazy = (segTree[rs].lazy + segTree[root].lazy) % mod;

        segTree[root].lazy = 0;
    }
}

void build(int root, int left, int right) {
    segTree[root].lazy = 0;
    if (left == right) {
        segTree[root].val = newVal[left];
        return;
    }
    int mid = left + (right - left) / 2;
    build(ls, left, mid);
    build(rs, mid + 1, right);
    pushUp(root);
}

void update(int root, int left, int right, int from, int to, int val) {
    if (from > right || to < left) {
        return;
    }
    if (from <= left && right <= to) {
        segTree[root].val =
            (segTree[root].val + val * (right - left + 1)) % mod;
        segTree[root].lazy = (segTree[root].lazy + val) % mod;
        return;
    }
    pushDown(root, left, right);
    int mid = left + (right - left) / 2;
    update(ls, left, mid, from, to, val);
    update(rs, mid + 1, right, from, to, val);
    pushUp(root);
}

int query(int root, int left, int right, int from, int to) {
    if (from > right || to < left) {
        return 0;
    }
    if (from <= left && right <= to) {
        return segTree[root].val;
    }
    pushDown(root, left, right);
    int mid = left + (right - left) / 2;
    return (query(ls, left, mid, from, to) +
            query(rs, mid + 1, right, from, to)) %
           mod;
}
/** ******************************************************************/

// 树链剖分模板
vector<vector<int>> graph(M);  // 图
vector<int> father(M);         // 父节点
vector<int> son(M);            // 重孩子
vector<int> size(M);           // 子树节点个数
vector<int> deep(M);           // 深度,根节点为1
vector<int> top(M);            // 重链的头,祖宗
vector<int> id(M);             // 剖分新id
int cnt = 0;                   // 剖分计数

void dfs1(int cur, int from) {
    deep[cur] = deep[from] + 1;  // 深度,从来向转化来
    father[cur] = from;          // 父节点,记录来向
    size[cur] = 1;               // 子树的节点数量
    son[cur] = 0;                // 重孩子 (先默认0表示无)
    for (int& to : graph[cur]) {
        if (to == from) {  // 避免环
            continue;
        }
        dfs1(to, cur);                    // 处理子节点
        size[cur] += size[to];            // 节点数量叠加
        if (size[son[cur]] < size[to]) {  // 松弛操作,更新重孩子
            son[cur] = to;
        }
    }
}

void dfs2(int cur, int grandfather) {
    top[cur] = grandfather;     // top记录祖先
    id[cur] = ++cnt;            // 记录剖分id
    newVal[cnt] = oldVal[cur];  // 映射到新值
    if (son[cur] != 0) {        // 优先dfs重儿子
        dfs2(son[cur], grandfather);
    }
    for (int& to : graph[cur]) {
        if (to == father[cur] || to == son[cur]) {
            continue;  // 不是cur的父节点,不是重孩子
        }
        dfs2(to, to);  // dfs轻孩子
    }
}

// 本题中未使用
int lca(int x, int y) {
    while (top[x] != top[y]) {  // 直到top祖宗想等
        if (deep[top[x]] < deep[top[y]]) {
            swap(x, y);  // 比较top祖先的深度,x始终设定为更深的
        }
        x = father[top[x]];  // 直接跳到top的父节点
    }
    return deep[x] < deep[y] ? x : y;  // 在同一个重链中,深度更小的则为祖宗
}
/** ******************************************************************/

void updatePath(int x, int y, int val) {
    while (top[x] != top[y]) {
        if (deep[top[x]] < deep[top[y]]) {
            swap(x, y);
        }
        update(1, 1, n, id[top[x]], id[x], val);
        x = father[top[x]];
    }
    if (deep[x] < deep[y]) {
        swap(x, y);
    }
    update(1, 1, n, id[y], id[x], val);
}

void updateTree(int root, int val) {
    update(1, 1, n, id[root], id[root] + size[root] - 1, val);
}

int queryPath(int x, int y) {
    int sum = 0;
    while (top[x] != top[y]) {
        if (deep[top[x]] < deep[top[y]]) {
            swap(x, y);
        }
        sum += query(1, 1, n, id[top[x]], id[x]);
        sum %= mod;
        x = father[top[x]];
    }
    if (deep[x] < deep[y]) {
        swap(x, y);
    }
    sum += query(1, 1, n, id[y], id[x]);
    return sum % mod;
}

int queryTree(int root) {
    return query(1, 1, n, id[root], id[root] + size[root] - 1);
}
/** ******************************************************************/

signed main() {
    int m, root;
    cin >> n >> m >> root >> mod;

    for (int i = 1; i <= n; i++) {
        cin >> oldVal[i];
    }

    // 该树编号 [1, n]
    // 本题仅仅说有边,未说方向
    for (int i = 1, u, v; i <= n - 1; i++) {
        cin >> u >> v;
        graph[v].emplace_back(u);
        graph[u].emplace_back(v);
    }

    // 树链剖分 重链
    dfs1(root, 0);
    dfs2(root, root);
    // 根据映射的newVal建树
    build(1, 1, n);

    for (int i = 1, ask; i <= m; i++) {
        cin >> ask;
        int from, to, val, from, to, subtree;
        if (ask == 1) {
            cin >> from >> to >> val;
            updatePath(from, to, val);
        } else if (ask == 2) {
            cin >> from >> to;
            cout << queryPath(from, to) % mod << endl;
        } else if (ask == 3) {
            cin >> subtree >> val;
            updateTree(subtree, val);
        } else {
            cin >> subtree;
            cout << queryTree(subtree) % mod << endl;
        }
    }

    return 0;
}

优先队列 + lambda表达式

using pii = pair<int,int>;
auto cmp = [&nums1, &nums2](const pii& a, const pii& b) {
		// 注意,默认是大顶堆,要逆向思考
		return nums1[a.first]+nums2[a.second] > nums1[b.first]+nums2[b.second];
};
// pair 记录两组数组的下标
priority_queue<pii, vector<pii>, decltype(cmp)> heap(cmp);

auto cmp = [&](const int& idx1, const int& idx2) {
    if (steps[idx1] != steps[idx2]) {
        return steps[idx1] > steps[idx2];
    }
    // A*启发式搜索
    int len1 = n-1 - idx1;
    int len2 = n-1 - idx2;
    return len1 > len2;
};
priority_queue<int, vector<int>, decltype(cmp)> q(cmp);

多路归并

力扣:373. 查找和最小的 K 对数字

力扣:378. 有序矩阵中第 K 小的元素

该题是很模板的题:

优先队列按照两个值的和从小到大排序

多路归并,可以理解为邻接表一样

  • 先初始化邻接表 => 对所有道路构造一个初始值

  • 每次从队列中获取最优解 => 每个最优解都来自一条路

  • 将这条路继续往下走一步 => 加入优先队列

一直保证优先队列的长度 <= 邻接表的初始长度

class Solution {
private:
    using pii = pair<int,int>;
    vector<vector<int>> ans;

public:
    vector<vector<int>> kSmallestPairs(vector<int>& nums1, vector<int>& nums2, int k) {
        int len1 = nums1.size();
        int len2 = nums2.size();
        // 无论道路,还是步数都是k以内
        len1 = min(k, len1);
        len2 = min(k, len2);

        auto cmp = [&nums1, &nums2](const pii& a, const pii& b) {
            // 注意,默认是大顶堆,要逆向思考
            return nums1[a.first]+nums2[a.second] > nums1[b.first]+nums2[b.second];
        };
        // pair 记录两组数组的下标
        priority_queue<pii, vector<pii>, decltype(cmp)> heap(cmp);
        
        // 这里设定nums2来开辟多路
        // 道路的开端是nums1的首元素
        for (int i = 0 ; i < len2; i++) {
            heap.push({0, i});
        }

        while (k-- && !heap.empty()) {
            int roadIdx = heap.top().second;
            int roadSteps = heap.top().first;
            heap.pop();
            ans.push_back({nums1[roadSteps], nums2[roadIdx]});

            // roadIdx 的索引下还未走完
            if (roadSteps+1 < len1) {
                heap.push({roadSteps+1, roadIdx});
            }
        }

        return ans;
    }
};

并查集

洛谷:P3367 【模板】并查集

力扣:990. 等式方程的可满足性

拓展:

判断共有几个集合?

方法:遍历1~n,if(i == bing[i]) cnt++;

class UnionFind {
private:
    bool flag;              // false[0~n-1], true[1~n]
    int n;
    vector<int> parent;

public:
    UnionFind(int n, bool flag = false):n(n), parent(n), flag(flag) {
        iota(parent.begin(), parent.end(), 0);
        if (flag) {
            parent.push_back(n);
        }
    }

    int find(int x) {   // 记忆化,路径压缩
        return x == parent[x] ? x : parent[x] = find(parent[x]);
    }

    void unite(int x, int y) {  // x合并到y中
        parent[find(x)] = find(y);
    }

    int getGroups() {   // 统计图中有多少个集合
        int cnt = 0;
        for (int i = flag; i < n+flag; i++) {
            cnt += (i == parent[i]);
        }
        return cnt;
    }
};

可图性判定

杭电:Degree Sequence of Graph G - 2454

可以构成图的判定

两个概念:

**1.度的序列:**若把图G的所有顶点的度数排成一个序列S,则称S为图G的度序列。

**2.序列是可图的:**一个非负整数组成的有限序列如果是某个无向图的度序列,则称该序列是可图的

Havel-Hakimi定理(贪心)

判定过程:

  1. 从大到小排序,若最大是0则可图
  2. 删去第一个(最大的)并【1,MAX】分别减一
  3. 若出现负数,return false;
  4. 返回第一步
bool Havel(vector<int>& arr) {
    int n = arr.size();
    sort(arr.begin(), arr.end(), greater<int>());
    // 有重边则另当别论
    if (arr[0] > n-1) {
        return false;
    }

    for (int i = 0; i < n; i++) {
        if (arr[0] == 0) {
            break;
        }
        for (int j = 1; j <= arr[0] && j < n; j++) {
            if (--arr[j] < 0) {
                return false;
            }
        }
        arr[0] = 0;
        sort(arr.begin(), arr.end(), greater<int>());
    }
    return true;
}

二分图最大匹配(匈牙利算法)

洛谷:P3386 【模板】二分图最大匹配

输入标准:

n:点数;

m:右点数

e:数(从左连接到右)

数据结构:邻接矩阵

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int M = 10+500;

int l, r;       //左右点数
int e;          //边数量
bool mp[M][M];  //邻接矩阵,记录是否可达即可(用bool)
int link[M];    //记录右边匹配左边的编号(初始-1)
bool vis[M];    //记录右边的点是否已访问

bool dfs(int leftt) {
    for (int i = 1; i <= r; i++) {
        //如果是左边往右有连接
        //并且右边未访问
        if (mp[leftt][i] && !vis[i]) {
            //记录访问
            vis[i] = true;
            //如果link没有更新
            //如果link更新了,再看dfs能不能让左边的点让位
            //  ||运算符,从左往右判断
            if (link[i] == -1 || dfs(link[i]) ) {
                //匹配成功,右边的点记录左边的坐标
                link[i] = leftt;
                //匹配成功返回true
                return true;
            }
        }
    }

    //右边每个点看下来,找不到匹配
    return false;
}
//匈牙利算法
int Hungary() {
    //考虑到有的题是有0号的
    memset(link, -1, sizeof(link));
    int ans = 0;

    for (int i = 1; i <= l; i++) {
        //每轮都要重新挨个查看右边的点
        memset(vis, false, sizeof(vis));
        //匹配成功一对,ans+1
        if (dfs(i))
            ans++;
    }

    return ans;
}

signed main (void) {
    cin >> l >> r >> e;
    memset(mp, false, sizeof(mp));

    for (int i = 1; i <= e; i++) {
        int a, b;
        cin >> a >>b;
        //算法从左边去匹配右边,不需要双向赋值
        mp[a][b] = true;
    }

    int ans;
    ans = Hungary();
    cout << ans << endl;

    system("pause");
    return 0;
}

最短路

(图论) 最短路_CUBE_lotus的博客-CSDN博客

算法描述时间复杂度(n:点数 m:边数)
Dijstra (朴素)适用于稠密图O(n^2)
Dijstra (堆优化)适用于稀疏图O(m*logn)
Floyd多源最短路O(n^3)
Bellman Frod边数限制(负权)O(n*m)
SPFA理论效率最高(负权)
(竞赛中正权图一直卡该算法)
O(k*m) (k一般为2)

练习题:

洛谷:P3371 【模板】单源最短路径(弱化版)

牛客:J-史东薇尔城_2022天梯赛 上理工选拔赛

力扣:743. 网络延迟时间

力扣:787. K 站中转内最便宜的航班

负环:

洛谷:P3385 【模板】负环

杭电:World Exhibition - 3592 (差分约束)

差分约束

差分约束是典型的数形结合的思想

将条件的数学表达式化为图形

具体到这里是将类似 A - B <= C 关系化为图的有权边,然后跑最短路求解

练习题:

杭电:World Exhibition - 3592

杭电:Intervals- 1384

poj:1201 – Intervals


一个小技巧:(其实最终还是靠理解分析)

存图 减数 ---> 被减数

  • A - B <= C 则求最短路

  • A - B >= C 则求最长路

/**
 * Intervals
 * 差分约束 求最长路
 */
#include <bits/stdc++.h>
using namespace std;
#define int long long

const int M = 10 + 50000;
const long long INF = 0x3f3f3f3f3f3f3f3f;

vector<vector<pair<int, int>>> graph(M);
vector<int> dis(M, -INF);  // 最长路,初始化为-INF
vector<bool> vis(M);

void spfa(int start) {
    dis[start] = 0;
    queue<int> q;
    q.push(start);
    vis[start] = true;
    while (!q.empty()) {
        int from = q.front();
        q.pop();
        vis[from] = false;

        for (auto& it : graph[from]) {
            int to = it.first, val = it.second;
            // 求最长路
            if (dis[from] + val > dis[to]) {
                dis[to] = dis[from] + val;
                if (!vis[to]) {
                    vis[to] = true;
                    q.push(to);
                }
            }
        }
    }
}

void init() {
    fill(dis.begin(), dis.end(), -INF);
    fill(vis.begin(), vis.end(), false);
    graph = vector<vector<pair<int, int>>>(M);
}

void solve(int n) {
    init();

    // [0, 50000] -> [1, 50001]
    // [left, right] 至少有val个点
    // [0, right] 到 [0, left-1] 之间至少有val  前缀和的思想
    // right - (left-1) >= val  (这里是>=后面要求最大路)
    for (int i = 1, left, right, val; i <= n; i++) {
        cin >> left >> right >> val;
        left += 1, right += 1;
        graph[left - 1].push_back({right, val});
        // 这里可以优化把起点和终点确定,减少总区间长度
    }

    // 隐藏的约束条件
    // 相邻点之前的范围是[0, 1] 目的是让线条上的点全部先后连接
    // 0 <= i - (i-1) <= 1
    // 这里也转化成 >= 和上面保持一致
    // i - (i-1) >= 0
    // (i-1) - i >= -1
    for (int i = 1; i <= 50001; i++) {
        graph[i].push_back({i - 1, -1});
        graph[i - 1].push_back({i, 0});
    }

    spfa(0);
	// 前缀和的思想
    cout << dis[50001] - dis[0] << endl;
}

signed main() {
    int n;
    while (cin >> n) {
        solve(n);
    }
    return 0;
}

拓扑排序

**思路:**寻找入度为0的点,将该点的出度和该出度对应的点断开。不断重复此操作。

部分ACM题要求按从小到大输出拓朴排序,因此可以使用优先队列priority_queue<int, vector<int>, greater<int>>head;

力扣: 802. 找到最终的安全状态

class Solution {
public:
    vector<int> eventualSafeNodes(vector<vector<int>>& graph) {
        int n = graph.size();

        // 出度点集
        vector<vector<int>>mp(n);
        // 入度度数
        vector<int>inDegree(n);

        // 该题逆向思维,建立反向图
        // 其他题目按要求正常建图
        for (int to = 0; to < n; to++) {
            for (auto& from : graph[to]) {
                // 记录from的出入to
                mp[from].push_back(to);
            }
            inDegree[to] = graph[to].size();
        }

        // 存储入度为0的点
        // 这里用优先队列是因为部分ACM题要求按从小到大输出拓朴排序
        priority_queue<int, vector<int>, greater<int>>head;
        for (int to = 0; to < n; to++) {
            if (inDegree[to] == 0) {
                head.push(to);
            }
        }

        while (!head.empty()) {
            // 根据出队顺序获得拓扑排序
            int topPoint = head.top();
            head.pop();
            // 将相连的边去除
            for (auto& to : mp[topPoint]) {
                inDegree[to]--;
                if (inDegree[to] == 0) {
                    head.push(to);
                }
            }
        }

        // 回到本题,记录拓扑排序后入度为0的点
        vector<int>ans;
        for (int topPoint = 0; topPoint < n; topPoint++) {
            if (inDegree[topPoint] == 0) {
                ans.push_back(topPoint);
            }
        }
        return ans;
    }
};

Tarjan 算法

(图论) Tarjan 算法_天赐细莲的博客-CSDN博客

强联通分量

Tarjan 算法

Kosaraju算法

常用于计算强联通分量

  • 建立正向和反向图
  • 先dfs反向图
  • 在正向图中逆序dfs反向图的结果,并记录每个点在哪个联通分量中

练习题:

杭电:迷宫城堡 - 1269

#include <bits/stdc++.h>
using namespace std;

const int M = 10 + 1 * 10000;
vector<vector<int>> orderGraph(M);    // 正向图
vector<vector<int>> reverseGraph(M);  // 反向图

bool vis[M];     // 是否访问
int scc[M];      // 强连通分量
stack<int> stk;  // 反向图递归的入栈顺序

void reverseDFS(int cur) {
    vis[cur] = true;
    for (int& nex : reverseGraph[cur]) {
        if (!vis[nex]) {
            reverseDFS(nex);
        }
    }
    stk.push(cur);
}

void orderDFS(int cur, int father) {
    vis[cur] = true;
    scc[cur] = father;
    for (int& nex : orderGraph[cur]) {
        if (!vis[nex]) {
            orderDFS(nex, father);
        }
    }
}

void solve(int n, int m) {
    // 记录联通分量的代表元素
    memset(scc, -1, sizeof(int) * (n + 1));
    for (int i = 1; i <= n; i++) {
        orderGraph[i].clear();
        reverseGraph[i].clear();
    }
    // 建图
    for (int i = 1; i <= m; i++) {
        int from, to;
        cin >> from >> to;
        orderGraph[from].push_back(to);
        reverseGraph[to].push_back(from);
    }

    // 遍历反向图
    memset(vis, false, sizeof(bool) * (n + 1));
    for (int i = 1; i <= n; i++) {
        if (!vis[i]) {
            reverseDFS(i);
        }
    }

    // 根据反向图的点,逆序遍历
    memset(vis, false, sizeof(bool) * (n + 1));
    while (!stk.empty()) {
        if (!vis[stk.top()]) {
            orderDFS(stk.top(), stk.top());
        }
        stk.pop();
    }

    // 回到本题,判断是否所有点都在一个联通分量中
    bool flag = true;
    for (int i = 1; i <= n && flag; i++) {
        flag = scc[1] == scc[i];
    }
    cout << (flag ? "Yes" : "No") << endl;
}

int main() {
    int n, m;
    while (cin >> n >> m) {
        if (n == 0 && m == 0) {
            break;
        }
        solve(n, m);
    }

    return 0;
}

2-sat

练习:

杭电:Let’s go home- 1824

杭电:Party - 3062

将关系抽象成图的两个点,建立图,跑强连通分量

查看两个互斥关系的scc

灵活设定当前点对应取反点的哈希关系,注意边的数量!!!

/**
 * https://acm.hdu.edu.cn/showproblem.php?pid=1824
 * Let's go home
 * 2- sat
 * 一定要注意偏移量啊!!!
 * 经过多次测试,边的数量少了,但是oj不报错,单纯超时
 * 1000 队伍 * 3 * 2
 */
#include <bits/stdc++.h>
using namespace std;

const int M = (10 + 1000) * 3 * 2;

vector<vector<int>> graph;
int low[M];
int dfn[M];
int timestamp;

int scc[M];
stack<int> stk;
bool inStk[M];

void init(int n) {
    // 一队三人 + 偏移
    // n *= 2 * 3;
    graph = vector<vector<int>>(M);
    timestamp = 1;
    memset(low, 0, sizeof(low));
    memset(dfn, 0, sizeof(dfn));
    memset(scc, 0, sizeof(scc));
    memset(inStk, false, sizeof(inStk));
}

void tarjan(int cur) {
    dfn[cur] = low[cur] = timestamp++;
    stk.push(cur);
    inStk[cur] = true;

    for (int nex : graph[cur]) {
        if (dfn[nex] == 0) {
            // 未访问则搜索一次
            tarjan(nex);
            low[cur] = min(low[cur], low[nex]);
        } else if (inStk[nex]) {
            // 在栈中,也要松弛一次
            low[cur] = min(low[cur], dfn[nex]);
        }
    }

    // 自己的dfn和low相同,则构成一个强联通分量
    if (dfn[cur] == low[cur]) {
        int x = -1;
        do {
            x = stk.top();
            stk.pop();
            inStk[x] = false;
            scc[x] = cur;
        } while (x != cur);
    }
}

void solve(int n, int m) {
    // n 是队伍数 每队3人
    init(n);

    for (int i = 1, a, b, c; i <= n; i++) {
        scanf("%d %d %d", &a, &b, &c);
        // 队长或队员留
        graph[a].push_back(b + n * 3);
        graph[a].push_back(c + n * 3);
        graph[b].push_back(a + n * 3);
        graph[c].push_back(a + n * 3);

        // 不留,反命题
        graph[a + n * 3].push_back(b);
        graph[a + n * 3].push_back(c);
        graph[b + n * 3].push_back(a);
        graph[c + n * 3].push_back(a);

        // 两个队员一起留
        graph[b].push_back(c);
        graph[c].push_back(b);
        graph[b + n * 3].push_back(c + n * 3);
        graph[c + n * 3].push_back(b + n * 3);
    }

    for (int i = 1, a, b; i <= m; i++) {
        scanf("%d %d", &a, &b);
        // ab矛盾,互斥
        graph[a].push_back(b + n * 3);
        graph[b].push_back(a + n * 3);
    }

    for (int i = 0; i < 2 * n * 3; i++) {
        if (dfn[i] == 0) {
            tarjan(i);
        }
    }

    bool flag = true;
    for (int i = 0; i < n * 3; i++) {
        if (scc[i] == scc[i + n * 3]) {
            flag = false;
            break;
        }
    }

    puts(flag ? "yes" : "no");
}

int main() {
    int n, m;
    while (scanf("%d %d", &n, &m) != EOF) {
        solve(n, m);
    }
    return 0;
}

哈希 Hash (散列函数)

手写hash

仿函数

std::hash 速度太慢了

struct MyHash {
    template <class T1, class T2>
    inline size_t operator() (const pair<T1, T2>& pair) const {
        size_t h1 = std::hash<T1>()(pair.first);
        size_t h2 = std::hash<T2>()(pair.second);
        return h1 ^ h2;
    }
};

unordered_set<pair<int,int>, MyHash> ust;
unordered_map<pair<int,int>, int, MyHash> ump;

函数指针法

auto myHash = [](const pair<int, int> &p) -> size_t {
    static hash<long long> hash_ll;
    return hash_ll(p.first + (static_cast<long long>(p.second) << 32));
};
unordered_set<pair<int, int>, decltype(myHash)> points(0, myHash);

链地址

力扣:705. 设计哈希集合

力扣:706. 设计哈希映射

class MyHashSet {
private:
    vector<list<int>> data; //邻接表
    static const int base = 769;    //取一个不大不小的素数
    static int hash (int key) {
        return key%base;
    }
public:
    //就是把数组data预先分配好base的大小,并初始化好空的链表
    MyHashSet():data(base) {};

    void add(int key) {
        int h = hash(key);
        for (auto it:data[h])
            if (it == key)  return ;
        data[h].push_back(key);
    }

    void remove(int key) {
        int h = hash(key);
        //这里用 : 会在erase报错
        //for (auto it:data[h])
        for (auto it = data[h].begin(); it != data[h].end(); it++) {
            if (*it == key) {
                data[h].erase(it);
                return ;
            }
        }
    }

    /** Returns true if this set contains the specified element */
    bool contains(int key) {
        int h = hash(key);
        for (auto it:data[h])
            if (it == key)  return true;
        return false;
    }
};

线性探测

#include<bits/stdc++.h>
using namespace std;

const int mod = 769;
// 大小与mod值对应
// 存储原始的key
int keyArr[mod];
// 存储存储自己的操作
int hashArr[mod];

// 线性探测
int myHash(const int key) {
    int h = key%mod;
    h += (h < 0) ? mod : 0;
    // 已有哈希值,且此处的哈希不是对应最原先的key
    while (hashArr[h] != 0 && keyArr[h] != key) {
        h = (h+1)%mod;  // 线性探测
    }
    return h;
}

int main () {
    // 存储
    int key = 114514;
    int h = myHash(key);
    keyArr[h] = key;
    hashArr[h] ++;

    // 使用
    key = rand();
    h = myHash(key);
    hashArr[h];

    return 0;
}

康托展开

class CantorExpansion {
private:
    int n;
    vector<int> factor;
    string s;

protected:
    inline void calcFactorial() {
        factor[0] = 1;
        for (int i = 1; i < factor.size(); ++i) {
            factor[i] = factor[i-1] * i;
        }
    }

public:
    CantorExpansion(int number) {
        string&& tmp = to_string(number);
        this->s = tmp;
        this->n = s.size();
        factor.resize(n+1);
        calcFactorial();
    }
    CantorExpansion(vector<int>& arr):n(arr.size()), factor(arr.size()+1) {
        calcFactorial();
    }
    CantorExpansion(char* str):n(strlen(str)), factor(strlen(str)+1) {
        while (*str != '\0') {
            this->s += *str;
            ++str;
        }
        calcFactorial();
    }
    CantorExpansion(string& s):CantorExpansion(&s[0]) {}

    int cantor() {
        int ans = 1;
        for (int i = 0; i < n; i++) {
            int coef = 0;
            for (int j = i+1; j < n; j++) {
                coef += (s[i] > s[j]);
            }
            ans += coef * factor[n-i-1];
        }
        return ans;
    }

    string reverseCantor(int k) {
        vector<char> nums;
        for (int i = 1; i <= n; i++){
            nums.push_back(i+'0');
        }
        string ans;
        k -= 1;
        for (int i = 1; i <= n; i++) {
            int t = k / factor[n-i];
            k %= factor[n-i];
            ans += nums[t];
            nums.erase(nums.begin()+t);
        }
        return ans;
    }
};

贪心算法

田忌赛马类

力扣:455. 分发饼干

思路:

先将两个数组排序(从小到大),然后以类似O(n^2)的思想

根据题意,选择一个数组作为外层,和里层一次比较,符合cnt++

但里层的下标不会在第二轮中初始化,沿用之前的值,得j == cnt

因此简化成一个while的双指针操作,慢指针就是答案

class Solution {
public:
    //g是孩子数量,s是饼干数量
    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());
        int child = 0, biscut = 0;
        while ( child < g.size() && biscut < s.size() ) {
            //饼干来满足小孩
            if (s[biscut] >= g[child])
                child++;    //只有满足条件,孩子才能向后
            biscut++;   //饼干每轮都要向后一个
        }
        return child;
    }
};

基于优先队列的贪心

力扣:630. 课程表 III

就是在已知集合内获得替换掉最值(每个值的占位权重相同)

class Solution {
public:
    int scheduleCourse(vector<vector<int>>& courses) {
        sort(courses.begin(), courses.end(), [&](vector<int>&a, vector<int>&b){
            // 按照结束时间递增
            return a[1] < b[1];
        });

        priority_queue<int>pq;
        int totalTime = 0;
        for (auto &arr : courses) {
            int durationTime = arr[0];
            int endTime = arr[1];

            if (totalTime + durationTime <= endTime) {
                // 加入队列,总时间合法
                pq.push(durationTime);
                totalTime += durationTime;
            } else if (!pq.empty() && pq.top() > durationTime) {
                // 总时间不合法
                // 若当前 < 队列中最大的 则替换掉该数据的占位
                totalTime -= pq.top();
                pq.pop();
                totalTime += durationTime;
                pq.push(durationTime);
            }
        }

        return pq.size();
    }
};

得到回文串的最少操作次数

力扣:5237. 得到回文串的最少操作次数 (小数据范围)

洛谷:P5041 HAOI2009 求回文串 (大数据范围)

题解:

贪心 & 证明 & 更大数据范围解法 - 得到回文串的最少操作次数 - 力扣(LeetCode) (leetcode-cn.com)

本质上是一个求逆序对数问题 - 得到回文串的最少操作次数 - 力扣(LeetCode) (leetcode-cn.com)

class Solution {
public:
    int minMovesToMakePalindrome(string s) {
        int n = s.size();

        int ans = 0;
        // 从左第一个开始贪心
        for (int i = 0; i < n/2; i++) {
            // 逆向找匹配的第一个字符
            // 注意:逆向的第一个位置是n-1
            int idx = s.rfind(s[i], n-1 -i);
            // 找到自身,说明是奇数个字符
            // 需要放中间,因此偏移一个位置
            // 记得回到原位置,继续贪心
            if (idx == i) {
                swap(s[i], s[i+1]);
                i--;
                ans++;
                continue;
            }

            for (int j = idx; j < n-i-1; j++) {
                swap(s[j], s[j+1]);
                ans++;
            }
        }

        return ans;
    }
};

博弈

巴什博弈

描述:

现在有一副N张牌的扑克牌,A和B两人先后依次可以抽取1~M张牌

规定获得最后一张牌的人获胜

思路:

无论先手如何行动,后者必然能把两人的总数凑成M+1的情况。

因此,当最后只剩余M+1的情况时,必然第二个人能拿完

【巴什博弈】什么?!小学数学题居然也有博弈问题?还是全英题目?不如小学生系列…_哔哩哔哩_bilibili

力扣:292. Nim 游戏

杭电:Public Sale - 2149 逆向巴什博弈

/**
 * https://acm.hdu.edu.cn/showproblem.php?pid=2149
 */
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n, m;  // m 总数 n 取数
    while (cin >> m >> n) {
        if (m % (n + 1) == 0) {
            cout << "none" << endl;
            continue;
        }
        // 可获胜 输出第一步可取方案
        // 需要策略取 (唯一方式走向必胜)
        if (m > n) {
            cout << m % (n + 1) << endl;
        } else {
            // 可一次性取完 >= m
            for (int i = m; i <= n; i++) {
                cout << i << " \n"[i + 1 > n];
            }
        }
    }
    return 0;
}

尼姆博弈

描述:

现在有任意堆物品,两个人依次从中拿取物品,每堆能拿任意件(最少1)。拿到最后一件物品的人获胜。

思路:

把每堆物品全部异或^,如果答案是0则先手必胜,否则必败。

每个值与nim值异或后比原值小,则表示这堆物品在第一轮可取

杭电:Being a Good Boy in Spring Festival - 1850

/**
 * https://acm.hdu.edu.cn/showproblem.php?pid=1850
 */
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    while (cin >> n, n) {
        vector<int> arr(n);
        for (int& x : arr) {
            cin >> x;
        }
        int nim = accumulate(arr.begin(), arr.end(), 0, bit_xor<int>());
        if (nim == 0) {
            cout << 0 << endl;
            continue;
        }

        int sum = 0;
        for (int x : arr) {
            // 注意运算符先后级
            sum += (x ^ nim) < x;
        }
        cout << sum << endl;
    }
    return 0;
}

威佐夫博弈

描述:

两堆物品,两种取物方案

  1. 可以在一堆中任意取
  2. 两堆同时取一样的数量

思路:

比较复杂,就是枚举出答案后找到和黄金分割比的关系

杭电:取石子游戏 - 1527

/**
 * https://acm.hdu.edu.cn/showproblem.php?pid=1527
 */
#include <bits/stdc++.h>
using namespace std;
int main() {
    const double gold = (1 + sqrt(5.0)) / 2.0;
    int n, m;
    while (cin >> n >> m) {
        int minn = min(n, m);
        int maxx = max(n, m);
        double k = maxx - minn;
        int t = k * gold;
        cout << ((t == minn) ? 0 : 1) << endl;
    }
    return 0;
}

规定取物品次数

杭电:Good Luck in CET-4 Everybody! - 1847

取石子游戏,规定取的数量

根据初始状态和可取数量,进行素数筛选的打表

#include <bits/stdc++.h>
using namespace std;

const int M = 10 + 1000;
bool vis[M];

int main() {
    // 0视为必败
    for (int i = 0; i <= 1000; i++) {
        // 当前必败,另一轮就必胜
        if (!vis[i]) {
            // 本题规定,取的数量都是2^k
            for (int j = 0; i + (1 << j) <= 1000; j++) {
                vis[i + (1 << j)] = true;
            }
        }
    }

    int n = 0;
    while (cin >> n) {
        cout << (vis[n] ? "Kiki" : "Cici") << endl;
        // 本题特殊规律,巴什博弈
        // cout << (n % 3 == 0 ? "Kiki" : "Cici") << endl;
    }
    return 0;
}

典型依赖于前一个状态 (DP && DFS)

力扣:1025. 除数博弈

自顶向下 记忆化dfs

class Solution {
private:
    vector<bool> vis;

    bool dfs(int cur) {
        // 已经记录过的必败的状态
        if (vis[cur]) {
            return false;
        }
        // 0 < x < 1 不存在,则1必败
        if (cur == 1) {
            return false;
        } 

        for (int pre = 1; pre < cur; pre++) {
            // 符合条件,且前者必败,则当前必胜
            if (cur % pre == 0 && !dfs(cur - pre)) {
                return true;
            }
        }

        vis[cur] = true;
        // 考虑完前面所有的转化还没有true
        return false;
    }

public:
    bool divisorGame(int n) {
        vis = vector<bool>(n + 1);
        return dfs(n);
    }
};

自底向上 dp

class Solution {
public:
    bool divisorGame(int n) {
        vector<bool> dp(n + 1);
        // 0 < x < 1 不存在,则1必败
        dp[1] = false;
        for (int i = 2; i <= n; i++) {
            // 遍历前者的所有可能
            for (int j = 1; j < i; j++) {
                // 符合条件,且前者必败,则当前必胜
                if (i % j == 0 && !dp[i - j]) {
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[n];
    }
};

SG函数

任何博弈都是状态的转移,虽说可以转化为动归

但是SG函数更巧妙的是可以转化为图的依赖关系

杭电:Good Luck in CET-4 Everybody! - 1847

杭电:Fibonacci again and again - 1848

杭电:S-Nim - 1536 SG函数配合nim游戏

力扣:1025. 除数博弈

/**
 * S-Nim SG函数配合nim游戏 混合题
 */
#include <bits/stdc++.h>
using namespace std;
const int M = 10 + 10000;
vector<int> sg;
vector<int> step;

void getSG(int cur) {
    if (sg[cur] != -1) {
        return;
    }
    vector<bool> vis(cur);
    for (int i = 0; i < step.size(); i++) {
        int pre = cur - step[i];
        if (pre < 0) {
            break;
        }
        getSG(pre);
        vis[sg[pre]] = true;
    }
    int k = 0;
    while (k < vis.size() && vis[k]) {
        k += 1;
    }
    sg[cur] = k;
}

int main() {
    int n;
    while (cin >> n, n) {
        sg = vector<int>(M, -1);
        step = vector<int>(n);
        for (int& x : step) {
            cin >> x;
        }
        sort(step.begin(), step.end());
        cin >> n;
        while (n--) {
            int k = 0;
            cin >> k;
            int nim = 0;
            for (int i = 0, x; i < k; i++) {
                cin >> x;
                getSG(x);
                nim ^= sg[x];
            }
            cout << (nim == 0 ? "L" : "W");
        }
        cout << endl;
    }

    return 0;
}

几何

三角形面积

百度百科:三角形面积公式

力扣:812. 最大三角形面积

PTA:题目详情 - 习题3-5 三角形判断 (pintia.cn)

海伦公式
已知三边 a , b , c 令 p = a + b + c 2 S = p ∗ ( p − a ) ∗ ( p − b ) ∗ ( p − c ) 已知三边 a,b,c \\ 令 p = {a + b +c \over 2} \\ S = \sqrt{p*(p-a)*(p-b)*(p-c)} 已知三边a,b,cp=2a+b+cS=p(pa)(pb)(pc)


矩阵表示法
已知三点 A ( a , b ) , B ( c , d ) , C ( e , f ) S = 1 2 ∗ ∣ a b 1 c d 1 e f 1 ∣ 已知三点A(a, b), B(c, d), C(e, f) \\ S = {1\over2}*\begin{vmatrix} a & b & 1 \\ c & d & 1 \\ e & f & 1\end{vmatrix} 已知三点A(a,b),B(c,d),C(e,f)S=21 acebdf111

向量积法
S = 1 2 ∗ ∣ 向量积 ∣ S = {1\over2}*\begin{vmatrix}向量积\end{vmatrix} S=21 向量积

向量积的应用

向量积_百度百科

叉积

**几何意义:**结果为一个向量,与原向量垂直

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-byNTFPxA-1659964908546)(https://bkimg.cdn.bcebos.com/pic/f3d3572c11dfa9ecc8f784b362d0f703908fc1bd?x-bce-process=image/resize,m_lfit,w_460,limit_1/format,f_auto)]


**代数意义:**模长:(在这里θ表示两向量之间的夹角(共起点)(0°≤θ≤180°),它位于这两个矢量所定义的平面上。)

模长的绝对值表示两条向量构成的平行四边形的面积

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SVIV6DSJ-1659964908548)(https://bkimg.cdn.bcebos.com/formula/ebd56d8e6e60ff56bf9530a0ae87df0a.svg)]


下图中p点,q点,r点 向量 p q {pq} pq,向量 q r {qr} qr 的向量积

  • 为正,则角度 < 180度 q r 相对 p q 左偏 {qr相对pq左偏} qr相对pq左偏
  • 为0,则角度 = 180度 (平行)
  • 为负,则角度 > 180度 q r 相对 p q 右偏 {qr相对pq右偏} qr相对pq右偏

587_1.png (789×430) (leetcode-cn.com)


不需要强记关系,如在凸包中只需要关系一致即可

// p0p1 x p0p2     共起点p0
inline int cross(vector<int>& p0, vector<int>& p1, vector<int>& p2) {
    int x0 = p1[0] - p0[0], y0 = p1[1] - p0[1];
    int x1 = p2[0] - p0[0], y1 = p2[1] - p0[1];
    return x0 * y1 - y0 * x1;
}
// p0p1 x p1p2     p1作为p0p1的终点,p0p1的起点
inline int cross(vector<int>& p0, vector<int>& p1, vector<int>& p2) {
    int x0 = p1[0] - p0[0], y0 = p1[1] - p0[1];
    int x1 = p2[0] - p1[0], y1 = p2[1] - p1[1];
    return x0 * y1 - y0 * x1;
}

练习题:

处理共线问题,判断是否为0

力扣:2280. 表示一个折线图的最少线段数

多边形面积

S = 1 2 ∗ ∑ i = 1 n ∣ x i y i x i + 1 y i + 1 ∣ i = 1... n ( 首尾绕成环也要算 ) S = {1\over2} * \sum^{n}_{i = 1} {\begin{vmatrix} x_i & y_i \\ x_{i+1} & y_{i+1} \end{vmatrix}} \\ i = 1 ... n (首尾绕成环也要算) S=21i=1n xixi+1yiyi+1 i=1...n(首尾绕成环也要算)

凸包问题

(几何) 凸包问题_天赐细莲的博客-CSDN博客


IOU

IOU(数学术语)_百度百科

简单说就是两块图像重叠部分

容斥原理


练习题:

力扣:223. 矩形面积

class Solution {
public:
    int computeArea(int ax1, int ay1, int ax2, int ay2, int bx1, int by1, int bx2, int by2) {	// 左下角 右上角
        int sumArea = (ax2-ax1)*(ay2-ay1) + (bx2-bx1)*(by2-by1);
        //投影原理
        int leftx = max(ax1, bx1), rightx = min(ax2, bx2);
        int upy = min(ay2, by2), downy = max(ay1, by1);
        int xLen = max(0, rightx-leftx), yLen = max(0, upy-downy);
        return sumArea - xLen * yLen; 
    }
};

直线的截距表示法

在确定斜率的情况下,直线的表示形式可以表示为 y = k x + b y = kx + b y=kx+b

在常见的矩阵类题目中,如果常常需要表示主对角线 mainDiagonal副对角线 subDiagonal

主对角线 mainDiagonal的斜率为 k = 1 k = 1 k=1 斜截式为 y = x + b y = x + b y=x+b 因此可以用 截距 b 截距b 截距b来表示直线 b = y − x b = y - x b=yx

副对角线 subDiagonal 的斜率为 k = − 1 k = -1 k=1 斜截式为 y = − x + b y = -x + b y=x+b 因此可以用 截距 b 截距b 截距b来表示直线 b = y + x b = y + x b=y+x


练习题:

力扣:1001. 网格照明

// y = x + b
inline int getMainDiagonal(const int x, const int y) {
    return y - x;
}
// y = -x + b
inline int getSubDiagonal(const int x, const int y) {
    return y + x;
}
// y = kx + b
inline double slopeInterceptForm(const double& x, const double& y, const double& k) {
    return y - k*x;
}

排列组合

公式

排列组合 百度百科

排列

A n m = n ( n − 1 ) ( n − 2 ) ⋅ ⋅ ⋅ ( n − m + 1 ) = n ! ( n − m ) ! 共 m 个因子累乘 n > = m A^{m}_{n} = n(n-1)(n-2)···(n-m+1) = {n!\over(n-m)!} \\[2ex] 共m个因子累乘\quad n >= m Anm=n(n1)(n2)⋅⋅⋅(nm+1)=(nm)!n!m个因子累乘n>=m


组合
C n m = A n m m ! = n ! m ! ( n − m ) ! C n m = C n n − m n > = m C^{m}_{n} = {A^{m}_{n}\over {m!}} = {n!\over {m!(n-m)!}} \\[2ex] C^{m}_{n} = C^{n-m}_{n} \qquad n >= m Cnm=m!Anm=m!(nm)!n!Cnm=Cnnmn>=m


计算举例:

举例 C 5 3 = C 5 2 计算 5 ∗ 4 ∗ 3 3 ∗ 2 ∗ 1 = 5 ∗ 4 2 ∗ 1 分子分母各 m 个数累乘 C 5 3 = 5 ∗ ( 5 − 1 ) ∗ ( 5 − 2 ) 1 ∗ 2 ∗ 3 举例 \qquad \qquad C^{3}_{5} = C^{2}_{5} \\[2ex] 计算 \qquad \qquad {5*4*3 \over 3*2*1} = {5*4 \over 2*1} \qquad 分子分母各m个数累乘 \\[2ex] C^{3}_{5} = {5*(5-1)*(5-2) \over 1*2*3} 举例C53=C52计算321543=2154分子分母各m个数累乘C53=1235(51)(52)


问题举例:

有1个A,2个B,3个C 共有多少种排列
6 ! 1 ! ∗ 2 ! ∗ 3 ! {6!\over 1! * 2! * 3!} 1!2!3!6!

inline int Combination(int m, int n) {
    int ans = (m <= n);
    m = min(m, n - m);
    for (int i = 0; i < m; i++) {
        ans *= n - i;
    }
    for (int i = 1; i <= m; i++) {
        ans /= i;
    }
    return ans;
}

母函数

一些求种类总数的题中,除了用背包来做,还能用母函数

例:砝码称重

有1g,2g,3g,…mg的砝码,求称出ng的种类数量

  • 若每个砝码只能取一次 (1表示不取)

( 1 + x 1 ) ∗ ( 1 + x 2 ) ∗ ( x + x 3 ) ∗ . . . ∗ ( 1 + x m ) (1+x^1)*(1+x^2)*(x+x^3)*...*(1+x^m) (1+x1)(1+x2)(x+x3)...(1+xm)

  • 若每个砝码不限次数

( 1 + x 1 + x 2 + x 3 + . . . ) ∗ ( 1 + x 2 + x 4 + x 6 + . . . ) ∗ . . . ∗ ( 1 + x m + x 2 m + x 3 m + . . . ) (1+x^1+x^2+x^3+...)*(1+x^2+x^4+x^6+...)*...*(1+x^m+x^{2m}+x^{3m}+...) (1+x1+x2+x3+...)(1+x2+x4+x6+...)...(1+xm+x2m+x3m+...)


普通母函数

练习题:

无限次取物品 (整数拆分)

杭电:Ignatius and the Princess III - 1028

蓝桥杯:砝码称重

有限次取物品(包含只取一次) (砝码称重)

The Balance

#include <bits/stdc++.h>
#define int long long
using namespace std;

void solve(int n) {
    vector<int> step(n);
    int sum = 0;
    for (int i = 0; i < n; i++) {
        cin >> step[i];
        sum += step[i];
    }

    vector<int> cur(sum+1);
    // 初始,0状态有1次可能
    cur[0] = 1;

    sort(step.begin(), step.end());
    // 优化一下第二层循环的循环次数
    int offset = 0;
    for (int i = 0; i < n; i++) {
        vector<int> nex(cur.size());
        // j表示前一轮状态,用来作用于k
        // 无线次数时还是要到sum,有限次才能优化成offset
        for (int j = 0; j <= offset; j++) {
            // 只取一次但是还用多重的写法就行了
            for (int k = 0; k <= 1*step[i] && j+k <= sum; k += step[i]) {
                // 砝码放左边,放右边(用减法+绝对值即可)
                nex[k+j] += cur[j];
                nex[abs(k-j)] += cur[j];
            }
        }
        // 一维滚动数组
        cur = move(nex);
        offset += step[i];
    }

    vector<int> ans;
    for (int i = 0; i <= sum; i++) {
        if (cur[i] == 0) {
            ans.push_back(i);
        }
    }

    cout << ans.size() << endl;
    for (int i = 0; i < ans.size(); i++) {
        cout << ans[i] << " \n"[i+1==ans.size()];
    }
}

signed main () {
    // 砝码称重
    int n;
    while (cin >> n) {
        solve(n);
    }
    return 0;
}

指数型母函数

计算排列,系数要化为分数类型

分母是阶乘,计算中提取出来,输出答案时要乘回去

由于涉及到分数相关,double都会存在精度问题

练习题:

杭电:排列组合 - 1521

n件物品,每件物品有cnt个,要取m个,求排列数

#include <bits/stdc++.h>
//#define int long long
using namespace std;

const int M = 10 + 10;

int factor[M];
// 预处理计算阶乘
void getFactorial() {
    factor[0] = 1;
    for (int i = 1; i < M; i++) {
        factor[i] = factor[i-1]*i;
    }
}

double cur[M], nex[M];
void solve(int n, int m) {
    memset(cur, 0.0, sizeof(cur));
    memset(nex, 0.0, sizeof(nex));

    vector<int> cnt(n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &cnt[i]);
    }

    for (int i = 0; i <= cnt[0]; ++i) {
        cur[i] = 1.0 / factor[i];
    }

    for (int i = 1; i < n; i++) {
        for (int j = 0; j <= m; j++) {
            // 字母排列,每个物品的步长均为1
            for (int k = 0; k <= cnt[i] && k+j <= m; k++) {
                // factor跟随新的一轮的系数
                nex[k+j] += cur[j] / factor[k];
            }
        }
        for (int j = 0; j <= m; j++) {
            cur[j] = nex[j];
            nex[j] = 0.0;
        }
    }

    // 本题存在double精度问题,测评不稳定
    printf("%.0lf\n", cur[m]*factor[m]);
}

signed main () {
    getFactorial();

    int n, m;
    while (scanf("%d %d", &n, &m) != EOF) {
        solve(n, m);
    }

    return 0;
}

排列例题

力扣:357. 统计各位数字都不同的数字个数

class Solution {
public:
    int countNumbersWithUniqueDigits(int n) {
        int ans = 0;
        for (int i = 1 ; i <= n; i++) {
            ans += 9 * Arrangement(i-1, 9);
        }
        // 补上0位的一个
        return ans + 1;
    }

private:
    inline int Arrangement(int m, int n) {
        int end = n-m+1;
        int ans = 1;
        while (n >= end) {
            ans *= n;
            n --;
        }
        return ans;
    }
};

52张牌(不考虑花色)选6张

题解 | #163小孩#_牛客博客 (nowcoder.net)

选6个不同数字
C 6 13 C_{6}^{13} C613

一个数字出现2次,其余均1次
C 13 5 ∗ C 5 1 C_{13}^{5} * C_{5}^{1} C135C51

一个数字出现3次,其余均1次
C 13 4 ∗ C 4 1 {C^{4}_{13} * C^{1}_{4}} C134C41

一个数字出现4次,其余均1次
C 13 3 ∗ C 3 1 {C^{3}_{13} * C^{1}_{3}} C133C31

两个数字出现2次,其余均1次
C 13 4 ∗ C 4 2 {C^{4}_{13} * C^{2}_{4}} C134C42

一个数字出现2次,一个数字出现3次,其余均1次
C 13 3 ∗ C 3 1 ∗ C 2 1 {C^{3}_{13} * C^{1}_{3}} * C^{1}_{2} C133C31C21

三个数字都出现两次
C 13 3 {C^{3}_{13}} C133

一个数字出现两次,一个数字出现四次
C 13 2 ∗ C 2 1 ∗ C 1 1 {C^{2}_{13} * C^{1}_{2} * C^{1}_{1}} C132C21C11

两个数字出现三次
C 13 2 {C^{2}_{13}} C132

sum = 18395

概率

random_shuffle(arr.begin(), arr.end());

Fisher-Yates 洗牌算法

力扣:384. 打乱数组

class Solution {
private:
    vector<int>ordered;
    vector<int>unordered;
public:
    Solution(vector<int>& nums) {
        this->ordered.resize(nums.size());
        this->unordered.resize(nums.size());
        copy(nums.begin(), nums.end(), ordered.begin());
        copy(nums.begin(), nums.end(), unordered.begin());
    }
    
    vector<int> reset() {
        return ordered;
    }
    
    vector<int> shuffle() {
        for (int i = 1; i < unordered.size(); i++) {
            int j = rand()%(i+1);	// 注意这里,要把当前位置也考虑到
            swap(unordered[i], unordered[j]);
        }
        return unordered;
    }
};

信息熵

信息熵_百度百科

经典问题 : 1000瓶水,1瓶毒药,若干兔子来试毒,怎么最快找到毒药?

力扣:458. 可怜的小猪

class Solution {
public:
    int poorPigs(int buckets, int minutesToDie, int minutesToTest) {
        int cnt = minutesToTest / minutesToDie;
        return ceil(log2(buckets) / log2(cnt+1));
    }
};

( n + 1 ) x = b − − − − − − > x = l o g n + 1 ( b ) = l o g e ( b ) / l o g e ( n + 1 ) (n+1)^x =b ------> x=log_{n+1}(b) = log_e(b)/log_e{(n+1)} (n+1)x=b>x=logn+1(b)=loge(b)/loge(n+1)

蓄水池抽样

实现原理

保证每个点的概率都是1/n

  • 第1个点取得的概率是 1/1
  • 第2个点取得的概率是 1/2;1/2的概率在前一个区域,该区域的有1个值,每个值获取的概率是 1/1
  • 第3个点取得的概率是 1/3;2/3的概率在前一个区域,该区域的有2个值,每个值获取的概率是 1/2
  • 第4个点取得的概率是 1/4;3/4的概率在前一个区域,该区域的有3个值,每个值获取的概率是 1/3
  • 第5个点取得的概率是 1/5;4/5的概率在前一个区域,该区域的有4个值,每个值获取的概率是 1/4
  • 。。。以此类推
  • 数学归纳法 可知n个点的状态下,每个点的获取概率都是1/n

应用场景

在数据量未知的情况下,防止大数据的空间占用

如:在未知的海量人数下进行抽奖

练习题:

力扣:382. 链表随机节点

力扣:398. 随机数索引

class Solution {
private:
    ListNode* head;
public:
    Solution(ListNode* head) {
        this->head = head;
        srand((unsigned)time(nullptr));
    }
    
    int getRandom() {
        int cnt = 1;
        int ans = -1;
        for (ListNode* p = head; p != nullptr; p = p->next) {
            if (rand()%cnt == 0) {
                ans = p->val;
            }
            cnt++;
        }
        return ans;
    }
};

随机抽取离散不重叠块

问题在于如何把一个随机数映射到某一个离散的个体中

方法是先计算前缀和再通过二分快速搜索该随机数在哪块的范围内

当然也可以用蓄水池算法

练习题:

力扣:528. 按权重随机选择

力扣:497. 非重叠矩形中的随机点

class Solution {
private:
    // 随机数种子
    mt19937 gen{random_device{}()};
    // 随机数生成器
    uniform_int_distribution<int> dis;
    vector<long long> pre;

public:
    Solution(vector<int>& nums) {
        pre.resize(nums.size());
        // 构造前缀和,无需哨兵位置,当然也可以有哨兵,注意前后的对应
        partial_sum(nums.begin(), nums.end(), pre.begin());
        // 闭区间[minn, maxx]
        dis = uniform_int_distribution<int>(1, pre.back());
    }
    
    int pickIndex() {
        // 0权重无位置,最大值对应最后
        int r = dis(gen) % pre.back() + 1;
        // 大于等于
        auto it = lower_bound(pre.begin(), pre.end(), r);
        return it - pre.begin();
    }
};

其他算法

摩尔投票

描述: 返回数组中占比超过一半的元素。若没有,返回 -1 。

时间复杂度为 O(N) 、空间复杂度为 O(1)

**思路:**运用一一抵消的思想

练习题:

力扣: 面试题 17.10. 主要元素

力扣:229. 求众数 II

class Solution {
public:
    //Boyer-Moore 投票算法
    int majorityElement(vector<int>& nums) {
        int sentinel = -1;      //哨兵元素
        int cnt = 0;            //票数累计器

        for (auto it : nums) {
            if (cnt == 0) {             //如果票数为0,则用当前的新结点作为哨兵
                sentinel = it;
                cnt = 1;
            } else if (sentinel == it)  //数值相同,票数+1
                cnt++;
            else                        //数值不同,票数-1
                cnt--;                  //就是相互抵消一票
        }

        return count(nums.begin(), nums.end(), sentinel) > (nums.size()>>1) ? sentinel : -1;
    }
};

离散化

专题:(2条消息) (化简复杂度) 离散化_天赐细莲的博客-CSDN博客_离散化复杂度

置换

置换(汉语词语)_百度百科 (baidu.com)

常见的一种处理数据的思想:

与离散化一样,只和数据的相对关系有关,于具体大小差值无关

借助线性代数理解:

现有矩阵A和B,有一置换作用矩阵C

A*C = E

B*C = D

此时D与E的相对关系,同原先B与A的相对关系

数组1:[3, 4, 1, 2, 0]

数组2:[4, 2, 1, 0, 3]

下标01234
数组134120
数组1置换为下标形式01234
数组242103
数组2以数组1的方式置换13240

练习题:

这一般只是一种化简数据的思想,一般都是混合在题目中接着考另一种算法

1713. 得到子序列的最少操作次数

将最长公共子序列化为最长递增子序列

2179. 统计数组中好三元组数目

化为借助树状数组求解逆序对的形式

一次遍历

获得最大值和次大值

力扣:747. 至少是其他数字两倍的最大数

pair<int,int> getMax12(const vector<int>& nums) {
    int n = nums.size();

    int firstMax = INT_MIN/2, secondMax = INT_MIN/2;
    int maxIdx = -1;
    // 一次遍历找到最大和次大值
    for (int i= 0; i < n; i++){
        if (nums[i] >= firstMax) {
            // 优先搜寻最大值
            // 此时先前的最大值变为了次大值
            secondMax = firstMax;
            firstMax = nums[i];
            maxIdx = i;
        } else if (nums[i] >= secondMax) {
            // 再考虑次大值
            secondMax = nums[i];
        }
    }

    return {firstMax, secondMax};
}

判断是否均配对 (处理奇偶)

力扣:6020. 将数组划分成相等数对

题意解读:数列中的数是否均成对出现 (是否有出现奇数的数)

class Solution {
public:
    bool divideArray(vector<int>& nums) {
        unordered_map<int, int> ump;
        int odd = 0;
        for (int num : nums) {
            ump[num] ^= 1;
            odd += 2*ump[num] -1;
        }
        return odd == 0;
    }
};

原地分离数组中的两类元素

例题:

力扣:905. 按奇偶排序数组

力扣:283. 移动零

class Solution {
public:
    vector<int> sortArrayByParity(vector<int>& nums) {
        int n = nums.size();
        for (int i = 0, j = 0; i < n; i++) {
            if (nums[i]%2 == 0) {
                swap(nums[i], nums[j++]);
            }
        }
        return nums;
    }
};

原地哈希

一个序列的元素可以与序列的索引形成映射

时间复杂度 O ( n ) {O(n)} O(n) 空间复杂度 O ( 1 ) {O(1)} O(1)

练习题:

找出序列中出现两次的元素

力扣:442. 数组中重复的数据

原地交换

两次遍历,第一次遍历将数值交换到与索引对用的位置

第二次遍历下来与索引位置不对应的元素则出现超过一次

关于时间复杂度:

while中要交换的目标位置最多被访问两次,且有些位置不会被访问到,因此是 O ( n ) {O(n)} O(n)

class Solution {
public:
    vector<int> findDuplicates(vector<int>& nums) {
        vector<int> ans;
        int n = nums.size();

        for (int i = 0; i < n; i++) {
            int idx = nums[i]-1;
            while (nums[i] != nums[idx]) {
                swap(nums[i], nums[idx]);
                idx = nums[i]-1; 
            }
        }

        for (int i = 0; i < n; i++) {
            if (nums[i] != i+1) {
                ans.push_back(nums[i]);
            }
        }

        return ans;
    }
};

负值标记

一次遍历,访问过一次则标记为负数

当第二次访问到是负数时,则表示之前出现过一次

class Solution {
public:
    vector<int> findDuplicates(vector<int>& nums) {
        int n = nums.size();

        vector<int> ans;
        for (int i = 0; i < n; i++) {
            int idx = abs(nums[i])-1;
            if (nums[idx] < 0) {
                ans.push_back(abs(nums[i]));
            } else {
                nums[idx] = -nums[idx];
            }
        }

        return ans;
    }
};

操作系统

银行家算法

基于贪心

操作系统中避免死锁的方法之一

力扣:502. IPO

class Solution {
public:
    /**
        k:最多k个项目
        w:本钱  只增不减
        profits:收益
        capital:成本
    */
    int findMaximizedCapital(int k, int w, vector<int>& profits, vector<int>& capital) {
        int n = profits.size();
        vector<pair<int, int>>vp(n);
        for (int i = 0; i < n; i++) {
            vp[i].first = capital[i];
            vp[i].second = profits[i];
        }
        //成本优先,收益在后
        sort(vp.begin(), vp.end());

        priority_queue<int>pq;
        int pos = 0;
        while(k--){
            for ( ; pos < n && vp[pos].first <= w; pos++) {
                pq.push(vp[pos].second);    //把当前资本可以获得利润放入一个堆
            }
            if (pq.empty()) break;  //可以理解是n < k
            w += pq.top();          //资本不断上升
            pq.pop();               //堆顶是当前可以获得的最大资本
        }

        return w;
    }
};

三色标记法

记忆化dfs的三种状态记录

力扣:802. 找到最终的安全状态

class Solution {
private:
    vector<int>color;
public:
    vector<int> eventualSafeNodes(vector<vector<int>>& graph) {
        int n = graph.size();
        color.resize(n, 0);
        //0表示未访问
        //1表示中间路段
        //2表示搜索完毕
        vector<int>ans;
        for (int i = 0; i < n; i++){
            if (haveCycle(graph, i) )
                ans.push_back(i);
        }

        return ans;
    }
private:
    bool haveCycle(vector<vector<int>>& graph, int cur){
        //记忆话的关键所在!!!
        //如果是程序开始到现在已访问过的点
        //则判断这个是否是无出度的安全点
        if (color[cur]) return color[cur] == 2;
        //标记在中间访问
        color[cur] = 1;

        for (auto &it : graph[cur]){
            //遇到无环可继续
            //遇到有环直接return
            if (!haveCycle(graph, it)){
                return false;
            }
        }

        //走到最后,没有被环return掉则表明这是个安全点
        color[cur] = 2;
        return true;
    }
};

人工智障

决策树

力扣:427. 建立四叉树

力扣:558. 四叉树交集

可进行二维前缀和优化

本题保证了总能分割成正方形

class Solution {
private:
    vector<vector<int>> grid;

public:
    Node* construct(vector<vector<int>>& grid) {
        this->grid = grid;

        return dfs(0, 0, grid.size(), grid.size());
    }

protected:
    // 从点[rs, cs] 到点[re, ce]
    Node* dfs(int rs, int cs, int re, int ce) {
        // 保持与最初始的结点相同
        int sample = grid[rs][cs];
        for (int i = rs; i < re; ++i) {
            for (int j = cs; j < ce; ++j) {
                // 不等则直接递归
                if (grid[i][j] != sample) {
                    return new Node(true, false,
                                    dfs(rs, cs, (rs + re) / 2, (cs + ce) / 2),
                                    dfs(rs, (cs + ce) / 2, (rs + re) / 2, ce),
                                    dfs((rs + re) / 2, cs, re, (cs + ce) / 2),
                                    dfs((rs + re) / 2, (cs + ce) / 2, re, ce));
                }
            }
        }
        // 是一个整体
        return new Node(grid[rs][cs], true);
    }
};



END

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天赐细莲

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值