相关学习方向思考
2025.8.18
- 针对于学习的路径,由于前面学习的很多知识并没有打下坚实的基础,然后那时候刷题也没有跟上,所以现在针对于相关学习,是看一节左神的课+自己补前面的一个知识点,我感觉可以先补前面的知识点,然后再看一节后面相关的课
2025.8.21
- 如果遇到一些题目超时了,有两个原因,第一种就是你这种方法本来就是不可行,第二种就是你被卡常了,就是有一个特判没有设置,你可以去尝试去用一个固定的常数去卡住他的超时线😄(亲试有效)
2025.8.27
- 有一些问题,如果有明确的思路去解决,但是从正面去解决的话感觉难以实现的话,可以尝试反着去思考这个问题,比如,如果让你去解决不递减序列的相关问题,试着看可不可以转化为不递增序列去解决😄
基础算法
单调队列
[!NOTE] 这是一次尝试
单调栈
逆序对
- 树状数组
使用树状数组来求逆序队,主要要进行离散化,应为可以 a[i] 的值很大,但是 数组大小有限,不离散化的话,可能树状数组的 tree数组存不下。
思路: 从数组的右边开始便利,访问到当前元素的时候,检查树状数组中求和(比当前元素小的元素),因为是从右往左便利,如果有比当前元素小的,肯定在数组中是在当前元素的右边。
##include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n;
int tree[N];
int lowbit(int i){
return i & -i;
}
void add(int i, int w){
while(i <= n){
tree[i] += w;
i += lowbit(i);
}
}
// 1 - r
int sum(int r){
int ans = 0;
while(r > 0){
ans += tree[r];
r-= lowbit(r);
}
return ans;
}
signed main(){
cin >> n;
vector<int> a(n);
for(int i = 0; i < n; i++) cin >> a[i];
// 离散化
vector<int> b(n);
b = a
sort(b.begin(), b.end());
map<int, int> mp;
for(int i = 0; i < n; i++) mp[b[i]] = i + 1;
// 求逆序队
int ans = 0;
for(int i = n - 1; i >= 0; i--){
int r = sum(mp[a[i]] - 1);
ans += r;
add(mp[a[i]], 1);
}
cout << ans << endl;
return 0;
}归并分治
分治的含义是 整体的答案 左边的答案 + 右边的答案
- 归并排序
归并排序是一个稳定的排序
- 整体的有序 是 左边有序 + 右边有序 + 合并过程
void merge(int l, int r){
int mid = (l + r) >> 1;
int i = l, j = mid + 1, t1 = 0;
while(i <= mid && j <= r){
help[t1++] = (a[i] <= a[j]) ? a[i++] :a[j++];
}
while(i <= mid) help[t1++] = a[i++];
while(j <= r) help[t1++] = a[j++];
for(int i = r; i >= l; i--){
a[i] = help[--t1];
}
}
void guibin(int l, int r){
if(l >= r) return;
int mid = (l + r) >> 1;
guibin(l, mid);
guibin(mid + 1, r);
merge(l, r);
}分治题目: leetcode-分治
随机快排
离散化
如果数据规模大,但是数据量小的话,我们可以进行离散化处理。
- 给每一个值一个编号
- 法一
vector<int> b(n);
b = a;
sort(b.begin(), b.end());
b.erase(unique(b.begin(), b.end()), b.end());
map<int, int> mp;
for(int i = 0; i < b.size(); i++){mp[b[i]] = i;}
类并查集
如果是分联通块进行计算,我们可以不使用并查集,使用一个简单的思路,给每一个联通块编号,从而来考虑每一个联通块之间的关系。
-
c++
##include<bits/stdc++.h> using namespace std; ##define int long long void __(){ int n, m1, m2; cin >> n >> m1 >> m2; vector<vector<int>> a1(n + 1); vector<vector<int>> a2(n + 1); for(int i = 0; i < m1; i++){ int u, v; cin >> u >> v; a1[u].push_back(v); a1[v].push_back(u); } for(int i = 0; i < m2; i++){ int u, v; cin >> u >> v; a2[u].push_back(v); a2[v].push_back(u); } // 进行编号 vector<int> col1(n + 1,0); vector<int> col2(n + 1, 0); auto dfs2 = [&](auto f,int u, int k) -> void{ col2[u] = k; for(auto x : a2[u]){ if(col2[x] == 0){ f(f,x,k); } } }; auto dfs1 = [&](auto f, int u, int k) -> int{ int cnt = 0; col1[u] = k; for(auto x : a1[u]){ if(col1[x] == 0){ if(col2[x] != k) cnt++; else cnt += f(f,x,k);} } return cnt; }; int ans = 0; for(int i = 1; i <= n; i++){ if(col2[i] == 0){ dfs2(dfs2,i,i); } if(col1[i] == 0){ ans += dfs1(dfs1,i,col2[i]); if(col2[i] < i) ans ++; // 在前面有代表节点的时候便已经访问过这个节点,但是在F图中这个 i 节点并没有被 初始化,说明前面的代表节点和这个节点之间应该要增加一条边 } } cout << ans << endl; } signed main(){ int _; cin >> _; while(_--) __(); }
快速幂
无 mod
int fpw(int a, int b){
int res = 1;
while(b){
if(b & 1) res *= a;
a *= a;
b >>= 1;
}
return res;
}有mod
int fpw(int a, int b, int mod){
int res = 1;
while(b){
if(b & 1){
res = res * a % mod;
}
a = a * a % mod;
b >>= 1;
}
return res % mod;
}乘法逆元
线性
inv[1] = 1;
for (int i = 2; i <= n; ++i) {
inv[i] = (long long)(p - p / i) * inv[p % i] % p;
}扩展欧几里得
费马小
x = fpw(a, b-2,b)数据结构
折半搜索
并查集
基础并查集
int father[N];
void init(){ for(int i = 0; i < N; i++) father[i] = i; }
int find(int x){
if(x != father[x]){
father[x] = find(father[x]);
}
return x;
}
void un(int x, int y){
int fx = find(x), fy = find(y);
if(fx != fy){
father[fx] = fy;
}
}带权并查集
个人感觉带权并查集有点类似于 树形DP,后续补充
'hello'
前缀和 + 差分
前缀和
一维
二维
树上前缀和
- 点权
- 边权
差分
用于快速解决区间修改问题
一维
如果存在 [l, r] 区间内的值进行范围修改,在差分数组上面
二维
树上差分
- 点差分
如查询一棵树上 节点被访问的次数
Ex: s 到 t 路径节点的访问修改
当前节点的权重,由于当前节点的权重与子节点有关,便可以很好的解释 上面这个差分访问修改
- 边差分
Ex: s 到 t 路径的边的访问修改
当前节点的权重,由于当前节点的权重与子节点有关,便可以很好的解释 上面这个差分访问修改
线段树
基础线段树
懒更新的意义是,如果这个大区间进行了修改,当访问到这个大区间的子区间时我们再去下发大区间的修改信息。
区间查询,区间变化,区间重置
##include<bits/stdc++.h>
using namespace std;
##define int long long
##define endl '\n'
##define fr first
##define sc second
const int N = 1e5 + 10;
int sum[4 * N];
int ad[4 * N];
int a[N];
int change[4 *N];
bool vis[4 * N];
/*
区间查询,区间变化,区间重置
*/
void build(int l, int r, int i){
if(l == r){
sum[i] = a[r];
return;
}
int mod = (l + r) >> 1;
build(l, mod, i << 1);
build(mod + 1, r, i << 1 | 1);
sum[i] = sum[i << 1] + sum[i << 1 | 1];
ad[i] = 0;
change[i] = 0;
vis[i] = false;
}
void down(int i, int ln, int rn){
if(vis[i]){
sum[i << 1] = change[i] * ln;
change[i << 1] = change[i];
vis[i << 1] = true;
sum[i << 1 | 1] = change[i] * rn;
change[i << 1 | 1] = change[i];
vis[i << 1 | 1] = true;
vis[i] = false;
}
if(ad[i] != 0){
sum[i << 1] += (ln * ad[i]);
ad[i << 1] += ad[i];
sum[i << 1 | 1] += (rn * ad[i]);
ad[i << 1 | 1] += ad[i];
ad[i] = 0;
}
}
void add(int wol, int wor, int wov, int l, int r, int i){
if(l >= wol && r <= wor){
sum[i] += (r - l + 1) * wov;
ad[i] += wov;
return;
}
int mid = (l + r) >> 1;
/*
l -> mid mid + 1-> r
*/
down(i, (mid - l + 1), (r - mid));
if(wol <= mid){
add(wol, wor, wov, l, mid, i << 1);
}
if(wor >= mid + 1){
add(wol, wor, wov, mid + 1, r, i << 1 | 1);
}
sum[i] = sum[i << 1] + sum[i << 1 | 1];
}
void cha(int wol, int wor, int wov, int l, int r, int i){
if(l >= wol && r <= wor){
sum[i] = (r - l + 1) * wov;
change[i] = wov;
vis[i] = true;
return;
}
int mid = (l + r) >> 1;
down(i, (mid - l + 1), (r - mid));
if(wol <= mid){
cha(wol, wor, wov, l, mid, i << 1);
}
if(wor >= mid + 1){
cha(wol, wor, wov, mid + 1, r, i << 1 | 1);
}
sum[i] = sum[i << 1] + sum[i << 1 | 1];
}
int query(int wol, int wor, int l, int r, int i){
if(wol <= l && r <= wor){
return sum[i];
}
int mid = (l + r) >> 1;
down(i, (mid - l + 1), (r - mid));
int ans = 0;
if(wol <= mid){
ans += query(wol, wor, l, mid, i << 1);
}
if(wor >= mid + 1){
ans += query(wol, wor, mid + 1, r, i << 1 | 1);
}
return ans;
}
signed main(){
/*
1 1 1 1 6
*/
int n;
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
build(1, n, 1);
int q1,q2;
cin >> q1;
while(q1--){
int t1, t2, t3;
cin >> t1 >> t2 >> t3;
add(t1, t2, t3,1,n,1);
}
int chl, chr, chv;
cin >> chl >> chr >> chv;
cha(chl, chr, chv, 1, n, 1);
cin >> q2;
while(q2--){
int l,r;
cin >> l >> r;
cout << query(l , r, 1, n, 1) << endl;
}
}区间重置 + 范围查询
##include<bits/stdc++.h>
using namespace std;
##define int long long
##define endl '\n'
##define PII pair<int,int>
##define fr first
##define sc second
/*
范围重置 + 范围查询(线段树)
*/
const int N = 1e5 + 10;
int Max[4 * N]; // 需要 4 * N
int a[N];
// 懒更新 (访问才往下推)
int change[4 * N];
bool update[4 * N];
/*
ad 表示当前需要下发的信息,当前节点已经修正
*/
// O(n)
void build(int l, int r, int i){
if(l == r){
Max[i] = a[r];
return;
}
int mid = (l + r) >> 1; // 最后一定会达到相等,不会出现越界情况
build(l, mid, i << 1);
build(mid + 1, r, i << 1 | 1);
Max[i] =max( Max[i << 1],Max[i << 1 | 1]);
change[i] = 0, update[i] = false;
}
// 懒信息下发
void down(int i, int ln, int rn){
if(update[i]){
update[i << 1] = true;
change[i << 1] = change[i];
Max[i << 1] = change[i];
update[i << 1 | 1] = true;
change[i << 1 | 1] = change[i];
Max[i << 1 | 1] = change[i];
update[i] = false;
}
}
// O(nlog(n)) 范围查询
int query(int wol, int wor, int l, int r, int i){
if(l >= wol && r <= wor){
return Max[i];
}
int ans = 0;
int mid = (l + r) >> 1;
down(i, mid - l + 1, r - mid);
if(mid >=wol){
ans =max(ans,query(wol, wor, l, mid, i << 1));
}
if(mid + 1<= wor){
ans = max(ans,query(wol, wor, mid + 1, r, i << 1 | 1));
}
return ans;
}
// 范围i增加
void up(int wol, int wor, int wov, int l, int r, int i){
if(wol <= l && r <= wor){
change[i] = wov;
update[i] = true;
Max[i] = wov;
return;
}
int mid = (l + r) >> 1;
down(i, mid - l + 1, r - mid);
if(wol <= mid){
up(wol, wor, wov, l, mid, i << 1);
}
if(wor >= mid + 1){
up(wol, wor, wov, mid + 1, r, i << 1 | 1);
}
Max[i] = max(Max[i << 1], Max[i << 1 | 1]);
// cout << i << " " << Max[i] << endl;
}
signed main(){
int n;
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
build(1, n, 1);
int q1;
cin >> q1;
while(q1--){
int l, r, v;
cin >> l >> r >> v;
up(l, r, v, 1, n, 1);
}
int q;
cin >> q;
while(q --){
int l, r;
cin >> l >> r;
cout << query(l, r, 1, n, 1) << endl;
}
}
范围修改 + 范围查询
##include<bits/stdc++.h>
using namespace std;
##define int long long
##define endl '\n'
##define PII pair<int,int>
##define fr first
##define sc second
/*
范围修改 + 范围查询(线段树)
*/
const int N = 1e5 + 10;
int sum[4 * N]; // 需要 4 * N
int a[N];
int ad[4 * N]; // 懒更新 (访问才往下推)
/*
ad 表示当前需要下发的信息,当前节点已经修正
*/
// O(n)
void build(int l, int r, int i){
if(l == r){
sum[i] = a[r];
return;
}
int mid = (l + r) >> 1; // 最后一定会达到相等,不会出现越界情况
build(l, mid, i << 1);
build(mid + 1, r, i << 1 | 1);
sum[i] = sum[i << 1] + sum[i << 1 | 1];
ad[i] = 0;
}
// 懒信息下发
void down(int i, int ln, int rn){
if(ad[i] != 0){
sum[i << 1] += ln * ad[i];
ad[i << 1] += ad[i];
sum[i << 1 | 1] += rn * ad[i];
ad[i << 1 | 1] += ad[i];
ad[i] = 0;
}
}
// O(nlog(n)) 范围查询
int query(int wol, int wor, int l, int r, int i){
if(l >= wol && r <= wor){
return sum[i];
}
int ans = 0;
int mid = (l + r) >> 1;
down(i, mid - l + 1, r - mid);
if(mid >=wol){
ans += query(wol, wor, l, mid, i << 1);
}
if(mid + 1<= wor){
ans += query(wol, wor, mid + 1, r, i << 1 | 1);
}
return ans;
}
// 范围i增加
void add(int wol, int wor, int wov, int l, int r, int i){
if(wol <= l && r <= wor){
ad[i] += wov; // 往下传递
sum[i] += wov*(r - l + 1);
return;
}
int mid = (l + r) >> 1;
down(i, mid - l + 1, r - mid);
if(wol <= mid){
add(wol, wor, wov, l, mid, i << 1);
}
if(wor >= mid + 1){
add(wol, wor, wov, mid + 1, r, i << 1 | 1);
}
sum[i] = sum[i << 1] + sum[i << 1 | 1];
}
signed main(){
int n;
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
build(1, n, 1);
int q1;
cin >> q1;
while(q1--){
int l, r, v;
cin >> l >> r >> v;
add(l, r, v, 1, n, 1);
}
int q;
cin >> q;
while(q --){
int l, r;
cin >> l >> r;
cout << query(l, r, 1, n, 1) << endl;
}
}线段树的势能分析
线段树区间合并
处理 子串 子数组(相互连接) 的信息
##include <bits/stdc++.h>
using namespace std;
##define int long long
##define fr first
##define sc second
using PII = pair<int, int>;
const int N = 1e5;
int a[N];
int sum[N << 2];
int tree0[N << 2];
int pre0[N << 2];
int la0[N << 2];
int tree1[N << 2];
int pre1[N << 2];
int la1[N << 2];
bool update[N << 2];
int up_data[N << 2];
bool reversed[N << 2];
void up(int i, int ln, int rn) {
pre0[i] = (pre0[i << 1] == ln) ? (pre0[i << 1] + pre0[i << 1 | 1]) : pre0[i << 1];
pre1[i] = (pre1[i << 1] == ln) ? (pre1[i << 1] + pre1[i << 1 | 1]) : pre1[i << 1];
la0[i] = (pre0[i << 1 | 1] == rn) ? (la0[i << 1] + pre0[i << 1 | 1]) : la0[i << 1 | 1];
la1[i] = (pre1[i << 1 | 1] == rn) ? (la1[i << 1] + pre1[i << 1 | 1]) : la1[i << 1 | 1];
tree0[i] = max({tree0[i << 1], tree0[i << 1 | 1], la0[i << 1] + pre0[i << 1 | 1]});
tree1[i] = max({tree1[i << 1], tree1[i << 1 | 1], la1[i << 1] + pre1[i << 1 | 1]});
sum[i] = sum[i << 1] + sum[i << 1 | 1];
}
void build(int l, int r, int i) {
reversed[i] = false;
update[i] = false;
if (l == r) {
tree0[i] = (a[r] == 0) ? 1 : 0;
tree1[i] = (a[r] == 1) ? 1 : 0;
sum[i] = a[r];
la0[i] = pre0[i] = tree0[i];
la1[i] = pre1[i] = tree1[i];
return;
}
int mid = (r + l) >> 1;
build(l, mid, i << 1);
build(mid + 1, r, (i << 1 | 1));
up(i, mid - l + 1, r - mid);
}
void down(int i, int ln, int rn) {
if (update[i]) {
if (up_data[i] == 1) {
pre1[i << 1] = la1[i << 1] = tree1[i << 1] = ln;
pre0[i << 1] = la0[i << 1] = tree0[i << 1] = 0;
pre1[i << 1 | 1] = la1[i << 1 | 1] = tree1[i << 1 | 1] = rn;
pre0[i << 1 | 1] = la0[i << 1 | 1] = tree0[i << 1 | 1] = 0;
sum[i << 1] = ln;
sum[i << 1 | 1] = rn;
update[i] = false;
update[i << 1] = update[i << 1 | 1] = true;
up_data[i << 1] = up_data[i << 1 | 1] = 1;
} else {
pre0[i << 1] = la0[i << 1] = tree0[i << 1] = ln;
pre1[i << 1] = la1[i << 1] = tree1[i << 1] = 0;
pre0[i << 1 | 1] = la0[i << 1 | 1] = tree0[i << 1 | 1] = rn;
pre1[i << 1 | 1] = la1[i << 1 | 1] = tree1[i << 1 | 1] = 0;
sum[i << 1] = 0;
sum[i << 1 | 1] = 0;
update[i] = false;
update[i << 1] = update[i << 1 | 1] = true;
up_data[i << 1] = up_data[i << 1 | 1] = 0;
}
reversed[i << 1] = reversed[i << 1 | 1] = false;
}
if (reversed[i]) {
swap(pre1[i << 1], pre0[i << 1]);
swap(la1[i << 1], la0[i << 1]);
swap(tree0[i << 1], tree1[i << 1]);
swap(pre1[i << 1 | 1], pre0[i << 1 | 1]);
swap(la1[i << 1 | 1], la0[i << 1 | 1]);
swap(tree0[i << 1 | 1], tree1[i << 1 | 1]);
sum[i << 1] = ln - sum[i << 1];
sum[i << 1 | 1] = rn - sum[i << 1 | 1];
reversed[i << 1] = !reversed[i << 1];
reversed[i << 1 | 1] = !reversed[i << 1 | 1];
reversed[i] = false;
}
}
void change(int jobl, int jobr, int w, int l, int r, int i) {
if (jobl <= l && r <= jobr) {
if (w == 0) {
pre0[i] = la0[i] = tree0[i] = (r - l + 1);
sum[i] = 0;
pre1[i] = la1[i] = tree1[i] = 0;
update[i] = true;
up_data[i] = w;
} else {
pre1[i] = la1[i] = tree1[i] = (r - l + 1);
sum[i] = (r - l + 1);
pre0[i] = la0[i] = tree0[i] = 0;
update[i] = true;
up_data[i] = w;
}
reversed[i] = false;
return;
}
int mid = (l + r) >> 1;
down(i, mid - l + 1, r - mid);
if (jobl <= mid) {
change(jobl, jobr, w, l, mid, i << 1);
}
if (jobr >= mid + 1) {
change(jobl, jobr, w, mid + 1, r, i << 1 | 1);
}
up(i, mid - l + 1, r - mid);
}
int query1(int jobl, int jobr, int l, int r, int i) {
if (jobl <= l && r <= jobr) {
return sum[i];
}
int ans = 0;
int mid = (l + r) >> 1;
down(i, mid - l + 1, r - mid);
if (jobl <= mid) {
ans += query1(jobl, jobr, l, mid, i << 1);
}
if (jobr >= mid + 1) {
ans += query1(jobl, jobr, mid + 1, r, i << 1 | 1);
}
return ans;
}
// 返回 [l, r] 范围上 被 [jobl, jobr] 影响的区域 的 信息
vector<int> query2(int jobl, int jobr, int l, int r, int i) {
if (jobl <= l && r <= jobr) {
return {tree1[i], pre1[i], la1[i]};
}
int mid = (l + r) >> 1;
down(i, mid - l + 1, r - mid);
vector<int> a1 = {0, 0, 0};
vector<int> a2 = {0, 0, 0};
if (jobl <= mid) {
a1 = query2(jobl, jobr, l, mid, i << 1);
}
if (jobr >= mid + 1) {
a2 = query2(jobl, jobr, mid + 1, r, i << 1 | 1);
}
int max_len = max(a1[0], a2[0]);
if (jobl <= mid && jobr >= mid + 1) {
max_len = max(max_len, a1[2] + a2[1]);
}
int prefix_len = (jobl <= l) ? a1[1] : 0;
/*
[l, r]
[jobl,jobr]
返回 [l, r] 范围上 被 [jobl, jobr] 影响的区域 的 信息
*/
if (jobl <= l && a1[1] == (mid - l + 1) && jobr >= mid + 1) {
prefix_len += a2[1];
}
int suffix_len = (jobr >= r) ? a2[2] : 0;
if (jobr >= r && a2[1] == (r - mid) && jobl <= mid) {
suffix_len += a1[2];
}
return {max_len, prefix_len, suffix_len};
}
void reverse(int jobl, int jobr, int l, int r, int i) {
if (jobl <= l && r <= jobr) {
reversed[i] = !reversed[i];
swap(pre1[i], pre0[i]);
swap(la1[i], la0[i]);
swap(tree0[i], tree1[i]);
sum[i] = (r - l + 1) - sum[i];
return;
}
int mid = (l + r) >> 1;
down(i, mid - l + 1, r - mid);
if (jobl <= mid) {
reverse(jobl, jobr, l, mid, i << 1);
}
if (jobr >= mid + 1) {
reverse(jobl, jobr, mid + 1, r, i << 1 | 1);
}
up(i, mid - l + 1, r - mid);
}
signed main() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
build(1, n, 1);
while (m--) {
int pos, l, r;
cin >> pos >> l >> r;
l++; r++;
if (pos == 0) {
change(l, r, 0, 1, n, 1);
} else if (pos == 1) {
change(l, r, 1, 1, n, 1);
} else if (pos == 2) {
reverse(l, r, 1, n, 1);
} else if (pos == 3) {
cout << query1(l, r, 1, n, 1) << endl;
} else if (pos == 4) {
cout << query2(l, r, 1, n, 1)[0] << endl;
}
}
return 0;
}
开点线段树
- 问题
- 支持很大的范围,但是查询次数少,查询区间小
时间复杂度 2 _ m _ log(n),用 cnt 来记录编号,看节点分支是否访问过。
线段树历史最值操作
标签回收 → 进行剪枝
笛卡尔树
倍增算法 + ST表
倍增算法其实是常用来树上向上跳转的,因为每一个节点的父亲节点是唯一的 ST表 核心公式:
表示 i 节点向上走 次方步的位置
倍增算法其实讲究的是逼近: 逼近某一个目标,然后在向前走ST[x][0]就是目标范围 牛客多校3-H 这道题目就很好的描述来逼近这种效果.
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define fr first
#define sc second
#define endl '\n'
using PII = pair<int,int>;
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);
int n, k;
cin >> n >> k;
vector<int> fath(n + 1, 0);
vector<vector<int>> dj(n + 1);
for(int i = 2; i <= n; i++){
cin >> fath[i];
dj[fath[i]].push_back(i);
}
vector<array<int,3>> t(k);
for(int i = 0; i < k; i++){
cin >> t[i][2] >> t[i][0] >> t[i][1];
}
vector<bool> vis(n + 1, 0);
vis[1] = 1;
int h = 30;
// cout << "h:" << h << endl;
vector st(n + 1, vector<int>(h + 1, 0));
vector<int> deep(n + 1, 0);
auto dfs = [&](auto ff, int u, int f) -> void{
deep[u] = deep[f] + 1;
for(auto x : dj[u]){
ff(ff, x, u);
}
};
dfs(dfs, 1, 0);
for(int i = 1; i <= n; i++){
st[i][0] = fath[i];
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= h; j++){
st[i][j] = st[st[i][j - 1]][j - 1];
}
}
auto get = [&](int x) -> int{
int p = x;
for(int i = h; i >= 0; i--){
if(!st[p][i]) continue;
if(!vis[st[p][i]]) p = st[p][i];
}
return st[p][0];
};
auto start = [&](int x, int f, int l, int r) -> void{
int p = x;
for(int i = h; i >= 0; i--){
if(!st[p][i]) continue;
if(deep[st[p][i]] - deep[f] > r - l + 1) x = st[p][i];
}
int tt = st[p][0];
while(tt != f && !vis[tt]){
vis[tt] = 1;
tt = st[tt][0];
}
};
for(int i = 0; i < k; i++){
auto x = t[i];
if(vis[x[2]]){
cout << x[0] << endl;
return 0;
}
int len = (x[1] - x[0] + 1);
int p1 = get(x[2]);
// cout << "p1:" << p1 << "\n";
int h = deep[x[2]] - deep[p1];
if(h <= len){
cout << x[0] + h - 1 << endl;
return 0;
}
start(x[2], p1, x[0], x[1]);
}
cout << -1 << "\n";
return 0;
}
图论
最短路问题
遇见过很多 最短路 问题,题目的意思一般都指向明确,就是(i j)的路径最小。
[!NOTE] 但是一般都是 最短路的扩展, ex:到达终点时候需要满足什么样的状态
题目链接:
-
个人认为这道题非常的典,题中还应用了一类 自定义点,来管理相同类别的点之间的跳转。
##include<bits/stdc++.h>
using namespace std;
##define int long long
##define fr first
##define sc second
##define endl '\n'
using PII = pair<int,int>;
void slove(){
int n, m, x;
cin >> n >> m >> x;
vector<int> a(n + 1, 0);
vector<array<int, 3>> g[2 * n + 1];
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= n; i++){
g[a[i] + n].push_back({i,0,0});
g[i].push_back({a[i] + n, x, 1 });
}
for(int i = 0; i < m; i++){
int u, v, w;
cin >> u >> v >> w;
g[u].push_back({v,w,1});
}
priority_queue< array<int,3>, vector<array<int,3>>, greater<array<int,3>>> q;
q.push({0,1,0});
vector<vector<int>> dist(2 * n + 1, vector<int>(3, INT_MAX));
vector<vector<bool>> vis(2 * n + 1,vector<bool>(3,false));
dist[1][0] = 0;
while(!q.empty()){
auto temp = q.top();
q.pop();
int v = temp[1],step = temp[2];
if(vis[v][step]) continue;
// cout << v << " " << w << endl;
vis[v][step] = 1;
for(auto[u, w, l] : g[v]){
// cout << u << " " << w << endl;
int op = (step + l) % 3;
if(dist[u][op] > dist[v][step] + w){
dist[u][op] = dist[v][step] + w;
q.push({dist[u][op],u, op});}}
}
if(dist[n][0] == INT_MAX){
cout << "-1" << endl;
}else{
cout << dist[n][0] << endl;
}
}
signed main(){
ios::sync_with_stdio(false);
cout.tie(0);
int t;
cin >> t;
while(t--) slove();
return 0;
}拓扑排序
适用于那些 节点状态有先后性的问题。
- 如果需要求 最大/最小的拓扑排序的话,可以将存储的 deque 改成 优先队列
signed main(){
int n;
cin >> n;
vector<vector<int>> a(n + 1);
vector<int> in(n + 1);
for(int i = 0; i < n; i++){
int u, v;
cin >> u >> v;
a[u].push_back(v);
in[v]++;
}
deque<int> q;
for(int i = 1; i <= n; i++){
if(in[i] == 0){q.push_back(i);}
}
while(!q.empty()){
int x = q.front(); q.pop_front();
cout << x << endl;
for(auto t : a[x]){
in[t]--;
if(in[t] == 0) q.push_back(t);
}
}
return 0;
}- 板子题:P1347
ford + Bellman-Ford
- ford 算法 核心实现公式: 时间复杂度: O()
for(int u = 0; u < n; u++){
for(int v = 0; v < n; v++){
for(int k = 0; k < n; k++){
dist[u][v] = min(dist[u][v], dist[u][k] + dist[k][v]);
}
}
}相关问题:
- 求图的传递闭包:
IMPORTANT
- Bellman-ford(SPFA)
为了解决Ford的时间复杂度过高的问题,提出了一个松弛操作
通过不断优化 dist(v) 的路径最小值,时间复杂度: O(mn)
的路径取最短路,但是你这个只能是以一个源点进行
[!WARNING] 由于你是从一个源点开始进行的,通过这个点检测点时候发现没有负环,只能说明从 S这个点出发不能抵达一个负环, 所以你要判断整个图上是否存在负环一般的手段都是建立超级源点
本文只介绍队列优化的Bellman-ford: SPFA
vector<vector<pair<int, int>>> g(n + 1);
vector<int> dist(n + 1, 0);
vector<int> cnt(n + 1, 0);
vector<bool> vis(n + 1, false); // 用来标记节点是不是在队列中,防止重复
bool spfa(int n, int s){
memset(dist, 0x3f, dist.size());
memset(cnt, 0, cnt.size());
dist[s] = 0; vis[s] = 1;
deque<int> q;
q.push(s);
while(!q.empty()){
int u = q.front(); q.pop_front();
vis[u] = 0;
for(auto {x, y} : g[u]){
if(dist[x] > dist[u] + y){
dist[x] = dist[u] + y;
cnt[x] = cnt[u] + 1;
if(cnt[x] >= n) return false;
if(!vis[x]) q.push_back(x), vis[x] = 1;
}
}
}
return true;
}[!TIP] 节点松弛 n - 1次就是极限了:因为相当于每一个节点都对这个节点进行了dist更新,不可能会有更多了.
负环 差分约束
形式
- 找到一组满足这种式子的解
根据 小于号 的不等式来建图
建图方式:
- 边权重更新:
- 点权重更新: 从那个节点出发,这个节点的权重设置为 0 将这个式子应用到 图中,如果图中存在负环就不存在满足的解。
- 负环判断: 如果这个节点进队列 所有节点的 个数 - 1
- 如果队列为空,dist[n] 便是一组解
- 一般设置一个 超级源点 来解决图中不连通问题
- 如果存在变量是定值,设置一个限制源点
最小环问题
同余最短路
介绍: n 个数字,用最小的数字作为基准,用它的余数来进行划分建图,然后跑最短路,将余数作为节点,除了基准的数字其他数字作为边权重,如果 (i + ) == j,说明 i, j之间有一条边
[!CAUTION] 思考: 同余最短路是用来解决什么问题?
常见问题:
-
有n个数字,每个数字有无限个,能组成 x(很大)的个数
数学
基础数论
- 任何一个整数 都可以分解为 有限个质数的乘积
则 N 的约数集合为
正约数的个数 :
- 质数筛的算法只需要
高斯消元
高斯消元 有三种类别,我们会逐一补充,目前先补充 异或消元,但是其实本质是不变的。
线性基
给出一个数组,请找出这个 数组的任意元素进行异或,可以得到多少种情况。
线性基的大小便决定了 能异或的多少种情况 (2 ^ n - 1)
##include<bits/stdc++.h>
using namespace std;
##define int long long
##define fr first
##define sc second
##define endl '\n'
using PII = pair<int,int>;
/*
线性基
*/
void _1(){
int n;
cin >> n;
vector<int> a(n + 1, 0);
for(int i = 1; i <= n; i++) cin >> a[i];
/*
查找每一位的 基
首先 从 每一个数 开始遍历,然后枚举 每一个位置,如果这个位置有 基,将这个数 变成 两者的异或值
*/
int h = 61;
bool zero = false;
vector<int> ans;
vector<int> base(h + 1, 0);
for(int i = 1; i <= n; i++){
for(int j = h; j >= 0; j--){
if(((a[i] >> j) & 1) == 1){
if(base[j] == 0){
base[j] = a[i];
ans.push_back(a[i]);
break;
}else a[i] = a[i] ^ base[j];
}
if(a[i] == 0) zero = true;
}
}
for(auto x : ans) cout << x << " ";
cout << endl;
if(zero) cout << "yes" << endl;
else cout << "no" << endl;
}- 高斯消元
void _2(){
int n;
cin >> n;
vector<int> a(n + 1, 0);
for(int i = 1; i <= n; i++) cin >> a[i];
int len = 1, h = 61; // 上面是确定的
bool zero = false;
vector<int> base(1,0);
for(int i = h; i >= 0; i--){
for(int j = len; j <= n; j++){
if(((a[j] >> i) & 1) == 1){
swap(a[len],a[j]);
break;
}
}
if(((a[len] >> i) & 1) == 1){
for(int k = 1; k <= n; k++){
if(k != len && ((a[k] >> i) & 1) == 1){
a[k] = a[k] ^ a[len];
}
}
len++;
}
}
len --;
if(len != n) zero = true;
for(int i = 1; i <= len; i++) cout << a[i] << " ";
cout << endl;
if(zero) cout << "yes" << endl;
else cout << "no" << endl;
}
常见考点:
求异或最大值
从 最高位开始枚举,来进行更新最大值
##include<bits/stdc++.h>
using namespace std;
##define int long long
##define fr first
##define sc second
##define endl '\n'
using PII = pair<int,int>;
signed main(){
int n;
cin >> n;
vector<int> a(n + 1, 0);
for(int i = 1; i <= n; i++) cin >> a[i];
vector<int> base(53,0);
for(int i = 1; i <= n; i++){
for(int k = 52; k >= 0; k--){
if(((a[i] >> k) & 1) == 1){
if(base[k] == 0){
base[k] = a[i];
break;
}else{
a[i] ^= base[k];
}
}
}
}
int mx = 0;
for(int i = 52; i >= 0; i--){
mx = max(mx, mx ^ base[i]);
}
cout << mx << endl;
return 0;
}求 异或 第 k 小值
##include<bits/stdc++.h>
using namespace std;
##define int long long
##define fr first
##define sc second
##define endl '\n'
using PII = pair<int,int>;
/*
线性基
*/
signed main(){
int n;
cin >> n;
vector<int> a(n + 1,0);
for(int i = 1; i <= n; i++) cin >> a[i];
auto swp = [&](int i, int j) -> void{
int t = a[i];
a[i] = a[j];
a[j] = t;
};
int len = 1;
for(int i = 53; i >= 0; i--){
for(int j = len; j <= n; j++){
if(((a[j] >> i) & 1) == 1){
swp(j, len);
break;
}
}
if(((a[len] >> i) & 1) == 1){
for(int j = 1; j <= n; j++){
if(j != len && (((a[j] >> i) & 1) == 1)){
a[j] ^= a[len];
}
}
len ++;
}
}
len --;
// for(int i = 1; i <= len; i++) cout << a[i] << " ";
// cout << endl;
bool zero = (len != n);
int m;
cin >> m;
int mx = (1 << len);
// cout << mx << endl;
for(int i = 0; i < m; i++){
int t;
cin >> t;
if(zero) t--;
if(t == 0 && zero) cout << "0" << "\n";
else if(t == 0 || t >= mx) cout << "-1" << "\n";
else{
int ans = 0;
// cout << t << endl;
for (int i = len, j = 0; i >= 1; i--, j++) {
if ((t & (1L << j)) != 0) {
ans ^= a[i];
}
}
cout << ans << "\n";
// cout << endl;
}
}
return 0;
}二项式定理
基本公式:
关键的组合公式:(杨辉三角)
C(n, k)的模版:
const int N = 1e5 + 10;
const int mod = 1e9 + 7;
vector<int> fac(N, 0); // 阶乘表
vector<int> inv(N, 0); // 逆元表
void get_inv() {
inv[1] = 1;
for (int i = 2; i <= N; ++i) {
inv[i] = (long long)(p - p / i) * inv[p % i] % p;
}
}
void get_fac() {
fac[1] = 1;
for(int i = 2; i <= N; i++){
fac[i] = i * fac[i - 1] % mod;
}
}
int c(int n, int k) {
return (((fac[n] * inv[k]) % mod) * inv[n - k]) % mod;
}扩展欧几里得
中国剩余定理
由扩展欧几里得算法(后续填完扩展欧几里得会补充)
鸽巢原理
动态规划
思考: 什么时候才是使用动态规划的时候呢?
区间DP
当实现 区间DP 的时候,如何将递归该迭代
- 首先枚举每一个长度的区间,然后在枚举每一个区间的开头(这样可以避免判断边界条件),然后判断这个区间的扩展,因为当你枚举到这个长度的区间的时候,你是一定枚举完这个区间的子区间的。
- 注意 子区间中存在一些特判的情况!
常用模版
// 枚举区间长度
for(int len = 2; len <= n; len++){
// 便利每一个该长度的区间
for(int l = 1, r = l + len - 1; r <= n; l++,r++){
// 区间分割点
for(int k = l + 1; k < r; k++){
}
}
}
计算几何
折尺定理
题目
包子丸-algorithm-1.3.15
条件: 给定 n 个木棒,判断这 n 根木棒能不能连接在两个点之间
需要满足条件:
相关概念:
- D 表示两点之间的距离
博弈论
字符串
字符串哈希
[!NOTE] 其实就是将字符串变成 整数来存储,将不定长的String类型变成定长的整数类型,可以在一些特殊场景减少内存
- 基于一个 base进制的数字并且让其自然溢出
- 转化的时候每一位的值从 1 开始
实现代码:
const int base = 1e9 + 7;
int value(string s){
// v[] 表示 s[i] 在新定义的string中的位置
int ans = v[s[0]];
for(int i = 1; i < s.size(); i++){
ans = ans * base + v[s[i]];
}
return ans;
}获得子串hash
实现代码
int base = 499 // 基数
int pow[N];
int hash[N];
void init(string s, int n){
pow[0] = 1;
for(int i = 1; i < n; i++){
pow[i] = pow[i - 1] * base;
}
hase[0] = base * pow[0];
// hash[i] = hash[i - 1] * base + (s[i] - 'a' + 1) * 1;
for(int i = 1; i < n; i++){
hash[i] = hash[i - 1] * base + (s[i] - 'a' + 1) * 1;
}
hase[l ... r] = hase[r] - hase[l] * base^(r - l + 1)
}
杂项
数据
通过数据量的给出,可以大概的估计是一个什么时间复杂度的算法,从而来推演这个算法的类型。
或( | ) 和 与( & )
对于或和与的一些讨论
- 或 是只要有一个 1,或值 中这个位置就一定会存在一个 1
可以考虑 这个位置的 一是否需要存在来进行按位判断
- 与 是需要全部都是1,与值 中这个位置就一定存在 1
最大公约数 和 最小公倍数 的关系
-
预处理两个数字的最大公约数
这个
g[y][x%y]其实就相当于一个 动规的转辗相除法的一个应用
for(int x = 0; x < N; x ++) g[x][0] = g[0][x] = g[x][x] = x;
for(int x = 1; x < N; x ++){
for(int y = 1; y < x; y++){
g[x][y] = g[y][x] = g[y][x % y];
}
}- 特殊性质 对于任何正整数 a1,a2,…,an 和它们的 GCD g=gcd(a1,a2,…,an),如果我们定义一个新数组 ai′=ai/g,那么新数组 a1′,a2′,…,an′ 的最大公约数必然是 1
lcm(a, b) * gcd(a, b) = a * b
a * b = 质数 → (a = 1, b 为 质数)
- 在进行 GCD 的过程中,随着 不断进行 GCD,GCD的值只可能不变或者变小
异或值
- 遇到异或值可以考虑 枚举每一个位置,看是不是每一个位置的变化会对答案有影响(规律性)
二进制
- 在二进制的位移中 1是会被默认当作 (int)类型的
需要 (1ll << j) 防止溢出
如 (1 << j) 会被默认为 int 类型
图的创新知识
- 存在多个 起点的时候,可以设置虚点。
- 遇到中位数(对顶堆)
- 树上的 两个节点之间 只存在一条最短路径
二分
如果当前 元素的个数大于了 数组的和的一半肯定是不成立的
区间异或问题
对于一个区间 [l, r] 来说, l ^ r 中的 最高位的 1,相当于 最高位后面的 1都可以由 [l, r] 中的值异或得到。
1
在计算机中会自动被默认设置为 int 型,所以如果有long long 的话,需要 改变 1 的类型, (long long)
矩阵问题
矩阵 行列异或
矩阵构造
- 通用思考:找一下 总的异或 的值的关系
- 尝试去构造矩阵
公约数 + 因子问题
经常遇到公约数和因子的问题,也存在一些常见的结论:
-
n个数字的公因子数量很少
-
往前走100步以内,肯定有不是自己的因子的数字