文章目录
前言说明
在线查看博客: 算法笔记(个人用)(不定期更新)_CUBE_lotus的博客-CSDN博客
博客主页:CUBE_lotus
哔哩哔哩:天赐细莲
交流email : 1539349804@qq.com
主攻OJ平台:天赐细莲 - 力扣(LeetCode) 欢迎来交流
本文导出时间:2022年8月8日
Note
文档编辑相关
语言相关
输入输出流
// 提高cin cout 速度
用这个不如老老实实判断数据范围用
scanf和printf
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 随机数最大值,一般为
0x7fffffff或0x7fff- M_PI 圆周率
3.14159265358979323846
算法相关
时间复杂度
下表为y总整理
| 数据范围 | 算法 |
|---|---|
| n ≤ 30 {n \le 30} n≤30 | 指数级别,dfs+剪枝,状压dp |
| n ≤ 1 e 2 = = > O ( n 3 ) {n \le 1e2 ==> O(n^3)} n≤1e2==>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)} n≤1e3==>O(n2),O(n2logn) | dp, 二分, 朴素dijkstra,朴素prim, bellman-ford |
| n ≤ 1 e 4 = = > O ( n ∗ n ) {n \le 1e4 ==> O(n*\sqrt{n})} n≤1e4==>O(n∗n) | 块状链表, 分块, 莫队 |
| n ≤ 1 e 5 = = > O ( n l o g n ) {n \le 1e5 ==> O(nlogn)} n≤1e5==>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)} n≤1e6==>O(n),常数小O(nlogn) | 单调队列, hash, 双指针, 并查集, kmp, AC自动机 sort, 树状数组,heap, dijstra, spfa |
| n ≤ 1 e 7 = = > O ( n ) {n \le 1e7 ==> O(n)} n≤1e7==>O(n) | 双指针, kmp, AC自动机, 线性素数筛 |
| n ≤ 1 e 9 = = > O ( n ) {n \le 1e9 ==> O(\sqrt{n})} n≤1e9==>O(n) | 素数判断 |
| n ≤ 1 e 18 = = > O ( l o g n ) {n \le 1e18 ==> O(logn)} n≤1e18==>O(logn) | 最大公约数, 快速幂 |
| n ≤ 1 e 1000 = = > O ( ( l o g n ) 2 ) {n \le 1e1000 ==> O((logn)^2)} n≤1e1000==>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)} n≤1e100000==>O(logk∗loglogk) | (k表示位数) 高精度加减, FFT/NTT |
排序
题单
注意以下单非本人整理
kuangbin:
[kuangbin带你飞]专题1-23 - Virtual Judge (csgrandeur.cn)
牛客:
宫水三叶(力扣为主):
Home · SharingSource/LogicStack-LeetCode Wiki · GitHub
ReseeCher(洛谷):
优秀讲师:
基础数学
素数
蔡勒(Zeller)公式
[ ] 表示取整 []{表示取整} []表示取整
w 星期, 0 到 7 ,周日到周一 w{星期, 0到7,周日到周一} w星期,0到7,周日到周一
c 年份的前两位 c{年份的前两位} c年份的前两位
y 年份后两位 y{年份后两位} y年份后两位
m 月份, 3 到 14 ( 1 , 2 月化为前一年的 13 , 14 月) m{月份,3到14(1,2月化为前一年的13,14月)} m月份,3到14(1,2月化为前一年的13,14月)
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)]+d−1)mod7
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)
2p−1(2p−1)
的形式,其中
p
p{}
p 为素数且
2
p
−
1
2^p-1{}
2p−1 为素数。
class Solution {
public:
bool checkPerfectNumber(int num) {
return num == 6 || num == 28 || num == 496 || num == 8128 || num == 33550336;
}
};
数根
将一正整数的各个位数相加(即横向相加)后,若加完后的值大于等于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
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=i∏jnums[l]=l=i∑jlognums[l]
练习题:
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. 搜索插入位置
力扣:69. x 的平方根
逼近类二分
278. 第一个错误的版本 - (二分) 逼近类二分搜索 - 第一个错误的版本 - 力扣(LeetCode) (leetcode-cn.com)
练习题:
基于树的二分
练习题:
基于二叉搜索树(BST)的性质
左子树 < root > 右子树 (一般没有相同的值)
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;
}
};
有序矩阵的二分
练习题:
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;
}
};
三分查找
用于凹凸区间找最值
练习题:
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,231−1]
( − 2 ∗ e 9 , 2 ∗ e 9 ) (-2*e^9, 2*e^9) (−2∗e9,2∗e9)
unsigned int0~4294967295[ 0 , 2 32 − 1 ] [0, 2^{32}-1] [0,232−1]
[ 0 , 4 ∗ e 9 ) [0, 4*e^9) [0,4∗e9)
long long
(8字节,64位)
long long-9223372036854775808 ~ 9223372036854775807[ − 2 63 , 2 63 − 1 ] [-2^{63}, 2^{63}-1] [−263,263−1]
( − 9 ∗ e 18 , 9 ∗ e 18 ) (-9*e^{18}, 9*e^{18}) (−9∗e18,9∗e18)
unsigned long long0 ~ 1844674407370955161[ 0 , 2 64 − 1 ] [0, 2^{64}-1] [0,264−1]
[ 0 , 1.8 ∗ 1 0 19 ) [0, 1.8*10^{19}) [0,1.8∗1019)
__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;
}
大小写字母的转换
| 大写字母 | 十进制 | 二进制 | 小写写字母 | 十进制 | 二进制 |
|---|---|---|---|---|---|
| A | 65 | 0100 0001 | a | 97 | 0110 0001 |
| B | 66 | 0100 0010 | b | 98 | 0110 0010 |
| C | 67 | 0100 0011 | c | 99 | 0110 0011 |
观察可得,大小写字母在二进制中只有第6位不同,也就是差32的理由
大小写字母互换
ch ^= 32;
统一大写
ch &= -33;
ch &= 95;
统一小写
ch |= 32;
格雷码(Gray Code)
百度百科:格雷码_百度百科 (baidu.com)
在一组数的编码中,若任意两个相邻的代码只有一位二进制数不同,且最大数与最小数之间也仅一位数不同
在电路中便于电位的跳变
力扣:89. 格雷编码
对称法
后4个由前四个轴对称构成
数值位一样,首位补1
| 下标 | 实际数值 | 3位典型格雷码 |
|---|---|---|
| 0 | 0 | 000 |
| 1 | 1 | 001 |
| 2 | 3 | 011 |
| 3 | 2 | 010 |
| 4 | 6 | 110 |
| 5 | 7 | 111 |
| 6 | 5 | 101 |
| 7 | 4 | 100 |
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位典型格雷码 |
|---|---|---|---|
| 0 | 000 | 0 | 000 |
| 1 | 001 | 1 | 001 |
| 2 | 010 | 3 | 011 |
| 3 | 011 | 2 | 010 |
| 4 | 100 | 6 | 110 |
| 5 | 101 | 7 | 111 |
| 6 | 110 | 5 | 101 |
| 7 | 111 | 4 | 100 |
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;
}
};
二进制枚举
适合数据范围较小的,类似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表
练习题:
/**
* 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;
}
树上倍增
练习题:
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. 字符串的排列
力扣: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 (a∗b)modc=((amodc)∗(bmodc))modc(加法乘法同理)
**原理:**根据a的二进制代码进行分治和暂存(O(logn))
力扣: 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)
洛谷:P1962 斐波那契数列 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
斐波那契数列
#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(n−1)+f(n−2)
[ 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(n−1)]=[1∗f(n−1)+1∗f(n−2)1∗f(n−1)+0∗f(n−2)]=[1110]∗[f(n−1)f(n−2)]
回归到最初的起始条件
[
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(n−1)]=[1110](n−1)∗[f(1)f(0)]
| 测试点 | AC1 | AC2 | AC3 | AC4 | AC5 | AC6 | AC7 | AC8 | AC9 | AC10 | 平均 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ijk | 时间/ms | 52 | 125 | 63 | 57 | 59 | 131 | 3 | 3 | 16 | 11 | 52.00 |
| 内存/KB | 608 | 604 | 628 | 572 | 760 | 824 | 472 | 468 | 740 | 640 | 631.60 | |
| ikj | 时间/ms | 37 | 87 | 45 | 40 | 42 | 92 | 3 | 2 | 12 | 8 | 36.80 |
| 内存/KB | 600 | 620 | 744 | 756 | 752 | 628 | 472 | 624 | 492 | 640 | 632.80 |
除法取模
/**
* molecular 分子
* denominator 分母
*/
int subMod(int molecular, int denominator, int mod) {
int inverseElement = binPow(denominator, mod-2, mod);
return (molecular * inverseElement) % mod;
}
字符串取特定字串
基本步骤
- 根据题意找出串中不符合条件的点(下标)
- 遍历点集(下标)(这就是分界点)
- 将串从该点分割左右两个字串
- 递归搜索两个字串
练习题:
// 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=true;
return;
}
visited[t.state]=1; // 标记为在在正向队列中
Q.push(t); // 入队
}
} else { // 在正向队列中判断
if (visited[t.state]!=2) { // 没在反向队列出现过
if(visited[t.state]==1) { // 该状态在正向向队列中出现过
各种操作;
found=true;
return;
}
visited[t.state]=2; // 标记为在反向队列中
Q.push(t); // 入队
}
}
}
}
A*
原理思路:
通过构造启发式函数,用优先队列代替普通队列,在每次BFS取出点的时候,取的是计算出来的预计最优解
构造方式很多
通常在地图中可以直接算曼哈顿距离
可以通过当前步数 + 预计的最优距离的混合计算
等等
教学视频:
A*寻路算法详解 #A星 #启发式搜索_哔哩哔哩_bilibili
练习题:
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
练习题:
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;
}
};
经典回溯
练习题:
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
最长递增子序列
力扣: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();
}
};
拓展题:
最长公共子序列
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;
}
};
二维前缀和
注意重叠部分
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型的无限个
思路:
- 竖着放1个
1*2dp[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
**问题描述:**给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。
核心思路是一个物品,有取或不取两种状态
取的化对占用一定容量,需要从小容量转化过来
视频讲解,B站:0/1背包问题-动态规划 Knapsack_problem Dynamic Programming
| V | W | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 6 | 3 | 0 | 0 | 0 | 6 | 6 | 6 | 6 |
| 10 | 1 | 0 | 10 | 10 | 10 | 16 | 16 | 16 |
| 5 | 2 | 0 | 10 | 10 | 15 | 16 | 16 | 21 |
| 10 | 4 | 0 | 10 | 10 | 15 | 16 | 20 | 21 |
区间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数
讲解:
练习题:
不含前导零且相邻两个数字之差至少为 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. 航班预订统计
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 1≤n,t,ai≤100000 数据再大需要先离散化
每个点与字符串的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;
}
双向链表运用
练习题:
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;
}
};
栈与队列
栈
单调栈
练习题:
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
视频讲解:
练习题:
不可覆盖字串数量
杭电:剪花布条 - 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=0∑len−1s[i]∗primelen−i−1
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]∗primelen−1+s[1]∗primelen−2+⋅⋅⋅+s[len−1]∗prime0
讲解:
[【微扰理论】Rabin-Karp + 二分搜索]([微扰理论]Rabin-Karp + 二分搜索 - 最长重复子串 - 力扣(LeetCode) (leetcode-cn.com))
uint64_t就是unsigned long long会自动取模练习题:
力扣:1044. 最长重复子串
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;
}
};
最小表示法
在循环同构串中,寻找最小字典序的串
方法:破环成链,用双指针在链上进行比较
练习题:
力扣: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);
}
};
树
树状数组
专题:
线段树 SegmentTree
专题:
字典树
练习题:
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 【模板】轻重链剖分树链剖分
* 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);
多路归并
该题是很模板的题:
优先队列按照两个值的和从小到大排序
多路归并,可以理解为邻接表一样
先初始化邻接表 => 对所有道路构造一个初始值
每次从队列中获取最优解 => 每个最优解都来自一条路
将这条路继续往下走一步 => 加入优先队列
一直保证优先队列的长度 <= 邻接表的初始长度
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;
}
};
图
并查集
拓展:
判断共有几个集合?
方法:遍历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定理(贪心)
判定过程:
- 从大到小排序,若最大是0则可图
- 删去第一个(最大的)并【1,MAX】分别减一
- 若出现负数,return false;
- 返回第一步
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;
}
二分图最大匹配(匈牙利算法)
输入标准:
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;
}
最短路
| 算法 | 描述 | 时间复杂度(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) |
练习题:
力扣:743. 网络延迟时间
负环:
洛谷:P3385 【模板】负环
杭电:World Exhibition - 3592 (差分约束)
差分约束
差分约束是典型的数形结合的思想
将条件的数学表达式化为图形
具体到这里是将类似
A - B <= C关系化为图的有权边,然后跑最短路求解
练习题:
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 算法
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
练习:
杭电: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值异或后比原值小,则表示这堆物品在第一轮可取
/**
* 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;
}
威佐夫博弈
描述:
两堆物品,两种取物方案
- 可以在一堆中任意取
- 两堆同时取一样的数量
思路:
比较复杂,就是枚举出答案后找到和黄金分割比的关系
杭电:取石子游戏 - 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. 最大三角形面积
海伦公式
已知三边
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,c令p=2a+b+cS=p∗(p−a)∗(p−b)∗(p−c)
矩阵表示法
已知三点
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右偏
不需要强记关系,如在凸包中只需要关系一致即可
// 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
多边形面积
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=21∗i=1∑n xixi+1yiyi+1 i=1...n(首尾绕成环也要算)
凸包问题
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=y−x
副对角线 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(n−1)(n−2)⋅⋅⋅(n−m+1)=(n−m)!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!(n−m)!n!Cnm=Cnn−mn>=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计算3∗2∗15∗4∗3=2∗15∗4分子分母各m个数累乘C53=1∗2∗35∗(5−1)∗(5−2)
问题举例:
有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
蓝桥杯:砝码称重
有限次取物品(包含只取一次) (砝码称重)
#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;
}
排列例题
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} C135∗C51一个数字出现3次,其余均1次
C 13 4 ∗ C 4 1 {C^{4}_{13} * C^{1}_{4}} C134∗C41一个数字出现4次,其余均1次
C 13 3 ∗ C 3 1 {C^{3}_{13} * C^{1}_{3}} C133∗C31两个数字出现2次,其余均1次
C 13 4 ∗ C 4 2 {C^{4}_{13} * C^{2}_{4}} C134∗C42一个数字出现2次,一个数字出现3次,其余均1次
C 13 3 ∗ C 3 1 ∗ C 2 1 {C^{3}_{13} * C^{1}_{3}} * C^{1}_{2} C133∗C31∗C21三个数字都出现两次
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}} C132∗C21∗C11两个数字出现三次
C 13 2 {C^{2}_{13}} C132sum = 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. 按权重随机选择
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;
}
};
离散化
置换
常见的一种处理数据的思想:
与离散化一样,只和数据的相对关系有关,于具体大小差值无关
借助线性代数理解:
现有矩阵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]
| 下标 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 数组1 | 3 | 4 | 1 | 2 | 0 |
| 数组1置换为下标形式 | 0 | 1 | 2 | 3 | 4 |
| 数组2 | 4 | 2 | 1 | 0 | 3 |
| 数组2以数组1的方式置换 | 1 | 3 | 2 | 4 | 0 |
练习题:
这一般只是一种化简数据的思想,一般都是混合在题目中接着考另一种算法
将最长公共子序列化为最长递增子序列
化为借助树状数组求解逆序对的形式
一次遍历
获得最大值和次大值
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};
}
判断是否均配对 (处理奇偶)
题意解读:数列中的数是否均成对出现 (是否有出现奇数的数)
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)
练习题:
找出序列中出现两次的元素
原地交换
两次遍历,第一次遍历将数值交换到与索引对用的位置
第二次遍历下来与索引位置不对应的元素则出现超过一次
关于时间复杂度:
在
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的三种状态记录
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);
}
};




1万+

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



