数据结构
栈,队列
- s t a c k < T > stack<T> stack<T>
- q u e u e < T > queue<T> queue<T>
滑动窗口
无重复字符的最长子串
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_set<char> us;
int left = 0, right = 0;
int out = 0;
while (right < s.size()) {
us.emplace(s[right++]);
out = max(out, right - left);
while (!us.empty() && us.count(s[right]))
us.erase(s[left++]);
}
return out;
}
};
滑动窗口中位数
class Solution {
public:
// 方法1 滑动窗
vector<double> medianSlidingWindow_1(vector<int>& nums, int k) {
int N = nums.size();
vector<double> med;
multiset<int> win(nums.begin(), nums.begin() + k);
auto mid = next(win.begin(), k / 2);
for (int i = k; i < N + 1; i++) { // N + 1 是为了最后一个
double medVal = double(*mid) + double(*next(mid, k % 2 - 1));
med.push_back(medVal / 2);
if (i == N) continue;
win.insert(nums[i]);
if (nums[i] < *mid) mid--;
if (nums[i - k] <= *mid) mid++;
win.erase(win.lower_bound(nums[i - k]));
}
return med;
}
// 方法2 堆
vector<double> medianSlidingWindow(vector<int>& nums, int k) {
int N = nums.size();
vector<double> med;
unordered_map<int, int> hash_tab;
priority_queue<int> smaller; // max heap
priority_queue<int, vector<int>, greater<int>> bigger; // min heap
int idx = 0;
while (idx < k) smaller.push(nums[idx++]);
for (int j = 0; j < k / 2; j++) {
bigger.push(smaller.top());
smaller.pop();
}
while (true) {
med.push_back(k & 1 ? smaller.top() :
0.5 * (double(smaller.top()) + double(bigger.top())));
if (idx == nums.size()) break;
int out = nums[idx - k];
int ins = nums[idx++];
int balance = 0; // is balanced
balance += out <= smaller.top() ? -1 : 1;
hash_tab[out]++;
if (!smaller.empty() && ins <= smaller.top()) {
balance++;
smaller.push(ins);
} else {
balance--;
bigger.push(ins);
}
// re-balanced heaps
if (balance < 0) {
smaller.push(bigger.top());
bigger.pop();
balance++;
}
if (balance > 0) {
bigger.push(smaller.top());
smaller.pop();
balance--;
}
// remove invalid nodes
while (hash_tab[smaller.top()]) {
hash_tab[smaller.top()]--;
smaller.pop();
}
while (!bigger.empty() && hash_tab[bigger.top()]) {
hash_tab[bigger.top()]--;
bigger.pop();
}
}
return med;
}
};
并查集
- 处理集合
class uf { // 并查集 (不可删除节点)
private:
int con;
vector<int> sz;
vector<int> pa;
public:
uf (int n) {
for (int i = 0; i < n; ++i) {
sz.push_back(1);
pa.push_back(i);
}
this->con = n;
}
void merge(int x, int y) { // union
// 按秩合并
int rx = find(x);
int ry = find(y);
if (rx == ry) return;
if (sz[rx] > sz[ry]) {
pa[ry] = rx;
sz[rx] += sz[ry];
} else {
pa[rx] = ry;
sz[ry] += sz[rx];
}
con--;
}
int find(int x) {
// 路径压缩
while (pa[x] != x) {
pa[x] = pa[pa[x]];
x = pa[x];
}
return x;
}
bool connect(int x, int y) {
return find(x) == find(y);
}
int count() {
return con;
}
};
STL 容器
- Set,Map,Priority_queue
- 慢,但是好用,能过很多题
倍增思想
- 顾名思义,备注就是步长成倍增加
- 一般都是 成 2倍
- 任何数都可以用 2的幂次方序列的和 来表示
区间最值查询 RMQ
- 数列区间的最大值:输入一串数字,给你 M 个询问,每次询问就给你两个数字 X,Y,要求你说出 X 到 Y 这段区间内的最大数
- Range Minimum/Maximum Query
线段树算法
预处理和查询 都是 O(logn)- 思想:使用区间dp出每个点起 2 k 2^k 2k长度的极值 大区间的极值 使用 小区间的极值构成!
- 方法1
Sparse Table (ST)
预处理 O(nlgn) 查询 O(1) - 定义 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示 从 i i i 为起点, 2 j 2^j 2j 为长度的 区间 的极值
#include<iostream>
#include<cmath>
#define max(a, b) (a > b ? a : b)
const int space = 1e5 + 7;
int dp[space][34] = { {} };
//dp[i][j]表示从i开始长度为2^(j-1)中的答案
// 当然不包括第i + 2^(j-1)的数据;
int lenth[34] = {};
int log_2[33] = {};
int N, M;
void ST(int N) {
for (int i = 1; i <= N; i++) scanf("%d", &dp[i][0]);
for (int idx = 1; lenth[idx] <= N; idx++) {
for (int now = 1; now + lenth[idx] <= N + 1; now++) {
// now ~ now + 2^(idx-1) ~ now + 2^(idx-1) + 2^(idx-1)
dp[now][idx] = max(dp[now][idx - 1],
dp[now + lenth[idx - 1]][idx - 1]);
}
}
}
int RMQ(int left, int right) {
int k = right - left + 1;
k = log(k) / log(2);//计算log2(k),从left+lenth[k]要小于等于right,否则会有不确定的数据
while (left + lenth[k] < right - lenth[k] + 1) k++;//注了这段代码也可以AC,写成这样更容易理解且更保险;
printf("%d\n", max(dp[left][k], dp[right - lenth[k] + 1][k]));
}
int main(void)
{
lenth[0] = 1;
for (int i = 1; i < 31; i++) lenth[i] = lenth[i - 1] << 1;
scanf("%d %d", &N, &M);
// preprocessing
ST(N);
while (M--) {
int left, right;
scanf("%d %d", &left, &right);
// query
RMQ(left, right);
}
return 0;
}
二叉树的最近公共祖先 LCA
- 除了后序遍历,(步长始终为1)
- 也可以采用 倍增思想 TODO
- 参考链接
树状数组 + 线段树
RMQ的树状数组的实现
#include <bits/stdc++.h>
using namespace std;
const int space = 1e6 + 7;
int A[space] = {}, B[space] = {}; // A树状数组 + B原数组
int N = 0, M = 0;
int lowest_bit(int n) {return n & (-n);}
void update(int pos, int data, int arr_len) {
for (int i = pos; i <= arr_len; i += lowest_bit(i)) { // 父节点
A[i] = max(A[i], data); // 往父节点上更新 即往大区间更新
}
}
int query(int left, int right) {
// 兄弟节点 从右往左 表示从右往左的多个相邻的子区间
int max_ans = INT_MIN, i = right;
while (i >= left) { // 兄弟节点往左递归
max_ans = max(max_ans, B[i]);
if (i - lowest_bit(i) + 1 < left) i--; // 都是单个元素 就按原数组计算最大值
else { // 仍可进行 往左兄弟节点 的递归
max_ans = max(max_ans, A[i]);
i -= lowest_bit(i);
}
}
return max_ans;
}
// 下面3个函数此题中不需要 用于元素更新(加减)
void add(int pos, int k) { // 用k更新pos处的元素
while (pos <= N) {
B[pos] += k;
pos += lowest_bit(pos); // 往父节点更新
}
}
int get(int pos) { // 获取数组pos处的前缀和(包括pos)
int res = 0;
while (pos > 0) {
res += B[pos];
pos -= lowest_bit(pos); // 往左兄弟接节点更新
}
return res;
}
int sum(int p1, int p2) {
return get(p2) - get(p1 - 1);
}
// Main Loop
int main() {
scanf("%d %d", &N, &M);
for (int i = 1; i <= N; i++) {
scanf("%d", &B[i]);
update(i, B[i], N);
}
while (M--) {
int x, y;
scanf("%d %d", &x, &y);
printf("%d\n", query(x, y));
}
return 0;
}
RMQ的线段树的实现
#include <bits/stdc++.h>
using namespace std;
const int space = 1e5 + 7;
long long N, M;
int nums[space] = {0};
struct Node {
int start, end, maxV; // [start, end], maxV
// 其实按照相同的递归方式的话 start和end是不必存储的
// 在递归入口参数处加上就行
} tr[4 * space]; // 线段树长度 长于 原数组长度 因为原数组只存在于 其叶节点
void build(int pos, int start, int end) {
if (start == end) {
tr[pos] = {start, end, nums[start]};
return;
}
int mid = start + (end - start) / 2;
int left = 2 * pos + 1;
int right = 2 * pos + 2;
build(left, start, mid);
build(right, mid + 1, end);
tr[pos] = {start, end, max(tr[left].maxV, tr[right].maxV)};
// cout << start << " " << end << " " << tr[pos].maxV << endl;
}
int query(int pos, int start, int end) {
if (tr[pos].end < start || tr[pos].start > end) return 0;
if (start <= tr[pos].start && tr[pos].end <= end) return tr[pos].maxV;
// int mid = tr[pos].start + (tr[pos].end - tr[pos].start) / 2; // 不需要 因为子节点存储了二分的区间
int maxV = INT_MIN;
maxV = query(pos * 2 + 1, start, end); // 左
return max(maxV, query(pos * 2 + 2, start, end)); // 右
}
// 下面这个更新函数在本题中不需要
void update(int cur, int idx, int val) { // 从cur开始 旨在将idx更新为val
if (tr[cur].start == tr[cur].end) {
nums[idx] = tr[cur].maxV = val;
return;
}
int left_node = 2 * cur + 1;
int right_node = 2 * cur + 2;
int mid = tr[cur].start + (tr[cur].end - tr[cur].start) / 2;
if (tr[cur].start <= idx && idx <= mid)
update(left_node, idx, val);
else update(right_node, idx, val);
tr[cur].maxV = max(tr[left_node].maxV, tr[right_node].maxV);
}
// Main Loop
int main() {
// // 下面两句是为了 加速输入输出流
// std::ios::sync_with_stdio(false); // 解除 stdio 和 cout / cin 的绑定 做了之后要注意不要同时混用cout和printf之类
// std::cin.tie(0); // 解除 cin 和 cout 的绑定 否则每次执行<<都要flush IO负担 0表示NULL
// cin >> N >> M; // TLE
// for (int i = 1; i <= N; i++) cin >> nums[i];
scanf("%d %d", &N, &M);
for (int i = 1; i <= N; i++) scanf("%d", &nums[i]);
build(1, 1, N);
int start, end;
while (M--) {
// cin >> start >> end; // TLE
// cout << query(1, start, end) << endl;
scanf("%d %d", &start, &end);
printf("%d\n", query(1, start, end));
}
return 0;
}
三大平衡树
- 参考 详见 三大平衡树 总结 + 模板
Treap
- 核心是 利用随机数的二叉排序树的各种操作复杂度平均为O(lgn)
#include <cstdio>
#include <cstring>
#include <ctime>
#include <iostream>
#include <algorithm>
#include <cstdlib>
#include <cmath>
#include <utility>
#include <vector>
#include <queue>
#include <map>
#include <set>
#define max(x,y) ((x)>(y)?(x):(y))
#define min(x,y) ((x)>(y)?(y):(x))
#define INF 0x3f3f3f3f
#define MAXN 100005
using namespace std;
int cnt=1,rt=0; //节点编号从1开始
struct Tree
{
int key, size, pri, son[2]; //保证父亲的pri大于儿子的pri
void set(int x, int y, int z)
{
key=x;
pri=y;
size=z;
son[0]=son[1]=0;
}
}T[MAXN];
void rotate(int p, int &x)
{
int y=T[x].son[!p];
T[x].size=T[x].size-T[y].size+T[T[y].son[p]].size;
T[x].son[!p]=T[y].son[p];
T[y].size=T[y].size-T[T[y].son[p]].size+T[x].size;
T[y].son[p]=x;
x=y;
}
void ins(int key, int &x)
{
if(x == 0)
T[x = cnt++].set(key, rand(), 1);
else
{
T[x].size++;
int p=key < T[x].key;
ins(key, T[x].son[!p]);
if(T[x].pri < T[T[x].son[!p]].pri)
rotate(p, x);
}
}
void del(int key, int &x) //删除值为key的节点
{
if(T[x].key == key)
{
if(T[x].son[0] && T[x].son[1])
{
int p=T[T[x].son[0]].pri > T[T[x].son[1]].pri;
rotate(p, x);
del(key, T[x].son[p]);
}
else
{
if(!T[x].son[0])
x=T[x].son[1];
else
x=T[x].son[0];
}
}
else
{
T[x].size--;
int p=T[x].key > key;
del(key, T[x].son[!p]);
}
}
int find(int p, int &x) //找出第p小的节点的编号
{
if(p == T[T[x].son[0]].size+1)
return x;
if(p > T[T[x].son[0]].size+1)
find(p-T[T[x].son[0]].size-1, T[x].son[1]);
else
find(p, T[x].son[0]);
}
int find_NoLarger(int key, int &x) //找出值小于等于key的节点个数
{
if(x == 0)
return 0;
if(T[x].key <= key)
return T[T[x].son[0]].size+1+find_NoLarger(key, T[x].son[1]);
else
return find_NoLarger(key, T[x].son[0]);
}
Splay-Tree 伸展树
- 双旋的过程就是一个建立相对平衡的二叉树的一个过程
- 模板代码:支持相同值,支持区间删除,支持懒惰标记
int cnt, rt;
int Add[MAXN];
struct Tree{
int key, num, size, fa, son[2];
}T[MAXN];
inline void PushUp(int x)
{
T[x].size=T[T[x].son[0]].size+T[T[x].son[1]].size+T[x].num;
}
inline void PushDown(int x)
{
if(Add[x])
{
if(T[x].son[0])
{
T[T[x].son[0]].key+=Add[x];
Add[T[x].son[0]]+=Add[x];
}
if(T[x].son[1])
{
T[T[x].son[1]].key+=Add[x];
Add[T[x].son[1]]+=Add[x];
}
Add[x]=0;
}
}
inline int Newnode(int key, int fa) //新建一个节点并返回
{
++cnt;
T[cnt].key=key;
T[cnt].num=T[cnt].size=1;
T[cnt].fa=fa;
T[cnt].son[0]=T[cnt].son[1]=0;
return cnt;
}
inline void Rotate(int x, int p) //0左旋 1右旋
{
int y=T[x].fa;
PushDown(y);
PushDown(x);
T[y].son[!p]=T[x].son[p];
T[T[x].son[p]].fa=y;
T[x].fa=T[y].fa;
if(T[x].fa)
T[T[x].fa].son[T[T[x].fa].son[1] == y]=x;
T[x].son[p]=y;
T[y].fa=x;
PushUp(y);
PushUp(x);
}
void Splay(int x, int To) //将x节点移动到To的子节点中
{
while(T[x].fa != To)
{
if(T[T[x].fa].fa == To)
Rotate(x, T[T[x].fa].son[0] == x);
else
{
int y=T[x].fa, z=T[y].fa;
int p=(T[z].son[0] == y);
if(T[y].son[p] == x)
Rotate(x, !p), Rotate(x, p); //之字旋
else
Rotate(y, p), Rotate(x, p); //一字旋
}
}
if(To == 0) rt=x;
}
int GetPth(int p, int To) //返回第p小的节点 并移动到To的子节点中
{
if(!rt || p > T[rt].size) return 0;
int x=rt;
while(x)
{
PushDown(x);
if(p >= T[T[x].son[0]].size+1 && p <= T[T[x].son[0]].size+T[x].num)
break;
if(p > T[T[x].son[0]].size+T[x].num)
{
p-=T[T[x].son[0]].size+T[x].num;
x=T[x].son[1];
}
else
x=T[x].son[0];
}
Splay(x, 0);
return x;
}
int Find(int key) //返回值为key的节点 若无返回0 若有将其转移到根处
{
if(!rt) return 0;
int x=rt;
while(x)
{
PushDown(x);
if(T[x].key == key) break;
x=T[x].son[key > T[x].key];
}
if(x) Splay(x, 0);
return x;
}
int Prev() //返回根节点的前驱 非重点
{
if(!rt || !T[rt].son[0]) return 0;
int x=T[rt].son[0];
while(T[x].son[1])
{
PushDown(x);
x=T[x].son[1];
}
Splay(x, 0);
return x;
}
int Succ() //返回根结点的后继 非重点
{
if(!rt || !T[rt].son[1]) return 0;
int x=T[rt].son[1];
while(T[x].son[0])
{
PushDown(x);
x=T[x].son[0];
}
Splay(x, 0);
return x;
}
void Insert(int key) //插入key值
{
if(!rt)
rt=Newnode(key, 0);
else
{
int x=rt, y=0;
while(x)
{
PushDown(x);
y=x;
if(T[x].key == key)
{
T[x].num++;
T[x].size++;
break;
}
T[x].size++;
x=T[x].son[key > T[x].key];
}
if(!x)
x=T[y].son[key > T[y].key]=Newnode(key, y);
Splay(x, 0);
}
}
void Delete(int key) //删除值为key的节点1个
{
int x=Find(key);
if(!x) return;
if(T[x].num>1)
{
T[x].num--;
PushUp(x);
return;
}
int y=T[x].son[0];
while(T[y].son[1])
y=T[y].son[1];
int z=T[x].son[1];
while(T[z].son[0])
z=T[z].son[0];
if(!y && !z)
{
rt=0;
return;
}
if(!y)
{
Splay(z, 0);
T[z].son[0]=0;
PushUp(z);
return;
}
if(!z)
{
Splay(y, 0);
T[y].son[1]=0;
PushUp(y);
return;
}
Splay(y, 0);
Splay(z, y);
T[z].son[0]=0;
PushUp(z);
PushUp(y);
}
int GetRank(int key) //获得值<=key的节点个数
{
if(!Find(key))
{
Insert(key);
int tmp=T[T[rt].son[0]].size;
Delete(key);
return tmp;
}
else
return T[T[rt].son[0]].size+T[rt].num;
}
void Delete(int l, int r) //删除值在[l, r]中的所有节点 l!=r
{
if(!Find(l)) Insert(l);
int p=Prev();
if(!Find(r)) Insert(r);
int q=Succ();
if(!p && !q)
{
rt=0;
return;
}
if(!p)
{
T[rt].son[0]=0;
PushUp(rt);
return;
}
if(!q)
{
Splay(p, 0);
T[rt].son[1]=0;
PushUp(rt);
return;
}
Splay(p, q);
T[p].son[1]=0;
PushUp(p);
PushUp(q);
}
Size Balance Tree
- SBT能够保证树的高度在lgn,这样对于插入,删除操作都能够准确保证时间复杂度在O(lgn)
- 详见 陈启峰SBT
int cnt, rt;
struct Tree
{
int key, size, son[2];
}T[MAXN];
inline void PushUp(int x)
{
T[x].size=T[T[x].son[0]].size+T[T[x].son[1]].size+1;
}
inline int Newnode(int key)
{
++cnt;
T[cnt].key=key;
T[cnt].size=1;
T[cnt].son[0]=T[cnt].son[1]=0;
return cnt;
}
void Rotate(int p, int &x)
{
int y=T[x].son[!p];
T[x].son[!p]=T[y].son[p];
T[y].son[p]=x;
PushUp(x);
PushUp(y);
x=y;
}
void Maintain(int &x, int p) //维护SBT的!p子树
{
if(T[T[T[x].son[p]].son[p]].size > T[T[x].son[!p]].size)
Rotate(!p, x);
else if(T[T[T[x].son[p]].son[!p]].size > T[T[x].son[!p]].size)
Rotate(p, T[x].son[p]), Rotate(!p, x);
else return;
Maintain(T[x].son[0], 0);
Maintain(T[x].son[1], 1);
Maintain(x, 0);
Maintain(x, 1);
}
inline int Prev() //返回比根值小的最大值 若无返回0
{
int x=T[rt].son[0];
if(!x) return 0;
while(T[x].son[1])
x=T[x].son[1];
return x;
}
inline int Succ() //返回比根值大的最小值 若无返回0
{
int x=T[rt].son[1];
if(!x) return 0;
while(T[x].son[0])
x=T[x].son[0];
return x;
}
void Insert(int key, int &x)
{
if(!x) x=Newnode(key);
else
{
T[x].size++;
Insert(key, T[x].son[key > T[x].key]);
Maintain(x, key > T[x].key);
}
}
bool Delete(int key, int &x) //删除值为key的节点 key可以不存在
{
if(!x) return 0;
if(T[x].key == key)
{
if(!T[x].son[0])
{
x=T[x].son[1];
return 1;
}
if(!T[x].son[1])
{
x=T[x].son[0];
return 1;
}
int y=Prev();
T[x].size--;
return Delete(T[x].key, T[x].son[0]);
}
else
if(Delete(key, T[x].son[key > T[x].key]))
{
T[x].size--;
return 1;
}
}
int GetPth(int p, int &x) //返回第p小的节点
{
if(!x) return 0;
if(p == T[T[x].son[0]].size+1)
return x;
if(p > T[T[x].son[0]].size+1)
return GetPth(p-T[T[x].son[0]].size-1, T[x].son[1]);
else
return GetPth(p, T[x].son[0]);
}
int GetRank(int key, int &x) //找出值<=key的节点个数
{
if(!x) return 0;
if(T[x].key <= key)
return T[T[x].son[0]].size+1+GetRank(key, T[x].son[1]);
else
return GetRank(key, T[x].son[0]);
}
树链剖分,树分治
- 国家集训队论文 <<分治算法在树的路径问题中的应用>>
- PDF链接
- 树分治和树上启发式合并的复杂度正确性的保证。
- LCA 最近公共祖先
搜索
深度优先
- 汉诺塔
宽度优先
- 迷宫
迭代+DFS
双向搜索
二分 查找
三分 查找
- 几何里面用得到
- 类似二分,不过是采用 三分 的方式
- 二分解决的是 单调有序的 数组查找 问题 (一维有序问题)
- 三分引申到二维(二次函数),得是凸函数
const double EPS = 1e-10;
double calc(double x) {
// f(x) = -(x-3)^2 + 2;
return -(x-3.0)*(x-3.0) + 2;
}
double ternarySearch(double low, double high) {
double mid, midmid;
while (low + EPS < high)
{
mid = (low + high) / 2;
midmid = (mid + high) / 2;
double mid_value = calc(mid);
double midmid_value = calc(midmid);
if (mid_value > midmid_value)
high = midmid;
else
low = mid;
}
return low;
}
A* , A星算法
- 启发式搜索
- 路径规划,计算最低通过成本
- f ( n ) = g ( n ) + h ( n ) f(n)=g(n)+h(n) f(n)=g(n)+h(n)
- 从起始搜索点到当前点的代价 + 当前结点到目标结点的估值
- 一种具有f(n)=g(n)+h(n)策略的启发式算法能成为A*算法的充分条件是:
- 搜索树上存在着从起始点到终了点的最优路径。
- 问题域是有限的。
- 所有结点的子结点的搜索代价值>0。
- h ( n ) = < h ∗ ( n ) h(n)=<h*(n) h(n)=<h∗(n) ( h ∗ ( n ) h*(n) h∗(n)为实际问题的代价值)。
字符串
- 字符串问题:子串、子序列、前后缀、字典序、回文串
- 前缀(preffix(str)):真前缀不包括字符串本身
- 后缀(suffix(str)):真后缀不包括字符串本身
匹配问题
- 单模式串/多模式串 匹配
- 暴力法 最坏是 O(N^2)
- 哈希法 / KMP 线性复杂度
字典树:在上下文中搜索多个模态
实现Trie 类形式
class Trie {
private:
bool isEnd;
Trie* next[26];
public:
/** Initialize your data structure here. */
Trie() {
isEnd = false;
memset(next, 0, sizeof(next));
}
/** Inserts a word into the trie. */
void insert(string word) {
Trie* cur = this;
for (auto& c : word) {
if (!cur->next[c-'a'])
cur->next[c-'a'] = new Trie();
cur = cur->next[c-'a'];
}
cur->isEnd = true;
}
/** Returns if the word is in the trie. */
bool search(string word) {
Trie* cur = this;
for (auto& c : word) {
cur = cur->next[c-'a'];
if (!cur) return false;
}
return cur->isEnd;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
bool startsWith(string prefix) {
Trie* cur = this;
for (auto& c : prefix) {
cur = cur->next[c-'a'];
if (!cur) return false;
}
return true;
}
};
单词搜索II
- 二维字符矩阵,搜索字典中的所有单词
class Solution {
public:
// Trie 树
struct TrieNode {
TrieNode *child[26];
string str; // 方便取字符串
TrieNode() : str{""} {
for (auto& c : child) c = NULL;
}
};
struct Trie {
TrieNode *root;
Trie() : root(new TrieNode()) {}
void insert(string s) {
TrieNode *p = root;
for (auto& a : s) {
int i = a - 'a';
if (!p->child[i]) p->child[i] = new TrieNode();
p = p->child[i];
}
p->str = s;
}
};
// 前缀树 + backtrack
vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
vector<string> res;
if (words.empty() || board.empty() || board[0].empty()) return res;
vector<vector<bool>> vis(board.size(), vector<bool>(board[0].size(), false));
Trie* T = new Trie();
for (auto& w : words) T->insert(w);
for (int i = 0; i < board.size(); i++) {
for (int j = 0; j < board[0].size(); j++) {
if (T->root->child[board[i][j] - 'a'])
search(board, T->root->child[board[i][j] - 'a'], i, j, vis, res);
}
}
return res;
}
void search(vector<vector<char>>& board, TrieNode* p, int i, int j,
vector<vector<bool>>& vis, vector<string>& res) {
// clear保证不重复
if (!p->str.empty()) {res.push_back(p->str); p->str.clear();}
int dirs[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
vis[i][j] = true;
for (auto& d : dirs) {
int y = i + d[0], x = j + d[1];
if (y < 0 || y >= board.size() || x < 0 || x >= board[0].size()) continue;
if (vis[y][x] || !p->child[board[y][x] - 'a']) continue;
search(board, p->child[board[y][x] - 'a'], y, x, vis, res);
}
vis[i][j] = false;
}
};
数组形式
int son[N][26],cnt[N],idx;
//cnt数组记录以当前节点为结尾的字符串的数目,idx为节点编号
int n;
void insert(string& s){
int n = s.length();
int p = 0;
for(int i = 0; i<n; i++){
int u = s[i]-'a';
if(!son[p][u]) son[p][u] = ++idx;
p = son[p][u];
}
cnt[p] ++;
}
int query(string &s){
int p = 0;
int n = s.length();
for(int i = 0; i<n; i++){
int u = s[i] - 'a';
if(!son[p][u]) return 0;
p = son[p][u];
}
return cnt[p];
}
KMP
前缀表/函数 理解
前缀函数 π \pi π (prefix function):
- π [ i ] \pi[i] π[i] 代表子串 s [ 0 … i ] s[0 \ldots i] s[0…i] 与其后缀相等的最长真前缀(proper prefix, 即 不包含本身的前缀) 。
- π [ i ] = max k = 0 … i { k : s [ 0 … k − 1 ] = s [ i − ( k − 1 ) … i ] } \pi[i]=\max _{k=0 \ldots i}\{k: s[0 \ldots k-1]=s[i-(k-1) \ldots i]\} π[i]=k=0…imax{k:s[0…k−1]=s[i−(k−1)…i]}
- “abaaab" − [ 0 , 1 , 0 , 1 , 2 , 2 , 3 ] -[0,1,0,1,2,2,3] −[0,1,0,1,2,2,3]
void prefix_table(string& pattern, vector<int>& prefix) {
int N = pattern.size();
prefix.resize(N, 0);
prefix[0] = 0;
int len = 0, idx = 1; // 分别表示最长前后缀 和 当前处理的位置
while (idx < N) {
if (pattern[idx] == pattern[len])
prefix[idx++] = ++len;
else {
if (len > 0) len = prefix[len - 1]; // 回退找最适合的位置
else prefix[idx++] = 0; // 找到头了 0
}
}
prefix.insert(prefix.begin(), -1); // 移下位 方便算法搜索
}
int kmp_search(string& text, string& pattern, vector<int>& prefix) {
int N = pattern.size(), M = text.size();
if (N == 0) return 0; // 空串
if (M == 0) return -1; // 空模板
prefix_table(pattern, prefix);
int i = 0, j = 0;
while (i < M) {
if (j == N - 1 && text[i] == pattern[j]) { // 找到一个
cout << "The Patten is at: " << i - j << endl;
j = prefix[j];
// return i - j;
}
if (text[i] == pattern[j]) { // 匹配中
i++; j++;
} else { // 不匹配需要移动对齐patten
j = prefix[j];
if (j == -1) {
i++; j++;
}
}
}
return -1;
}
DP的写法
class Solution {
public:
int strStr(string haystack, string needle) {
if (needle.empty()) return 0;
vector<vector<int>> dp(needle.size(), vector<int>(256, 0));
KMPCreator(needle, dp);
int stat = 0;
for (int i = 0; i < haystack.size(); ++i) {
stat = dp[stat][haystack[i]];
if (stat == needle.size()) return i - stat + 1;
}
return -1;
}
private:
void KMPCreator(string& needle, vector<vector<int>>& dp) {
int shadow = 0;
dp[0][needle[0]] = 1;
for (int i = 1; i < needle.size(); ++i) {
for (int c = 0; c < 256; c++)
dp[i][c] = dp[shadow][c];
dp[i][needle[i]] = i + 1;
shadow = dp[shadow][needle[i]];
}
}
};
ExKMP
- ExKMP,国外叫 “Z Algorithm"
- z [ i ] z[i] z[i] 是 s t r str str 和 从 i i i 开始的 s t r str str 的后缀的 最大公共前缀 长度
-
i
=
=
0
i == 0
i==0 处的值无意义,不考虑!
- Z函数的计算方法,朴素O(N^2)
vector<int> z_function_trivial(string s) {
int n = (int)s.length();
vector<int> z(n);
for (int i = 1; i < n; ++i)
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++z[i];
return z;
}
- 改进后 O(N)
- 最优匹配段:在所有已探测到的匹配段中,我们将保持结尾最靠右的那一个 [ l : r ] [l : r] [l:r]
- 下标 r r r可被认为是字符串 s s s已被算法扫描的边界;任何超过该点的字符都是未知的
vector<int> z_function(string s) {
// 最右匹配段 [l : r]
int n = (int)s.length();
vector<int> z(n);
for (int i = 1, l = 0, r = 0; i < n; ++i) {
if (i <= r) z[i] = min(r - i + 1, z[i - l]);
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++z[i];
if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
}
return z;
}
应用
- 查找子串:在text中查找pattern,① 构建新的字符串
s = text + 分隔符 + patten
PS:分隔符不在 text和pattern中!
- 本质不同子串查找:两个子串不同,当且仅当有这两个子串长度不一样 或者长度一样且有任意一位不一样;
- 字符串压缩:寻找一个最短的字符串 ,使得 t 可以被 s 的一份或多份拷贝的拼接表示,实现了对 s 的压缩!(解法的有效性 证明 同 kmp一样!)
自动机
- 确定有限状态自动机,一种数学模型
- 包括:字符集、状态集(起始状态+接收状态)、转移函数
- 状态 + 字符接受(动作)
- 包括:字典树 + KMP自动机 + AC自动机 + 回文自动机 + 序列自动机
AC自动机:AC 自动机其实就是 Trie 上的自动机
- Aho-Corasick automaton
- 可以认为是 Trie + KMP,两个步骤:
- 基础的 Trie 结构:将所有的模式串构成一棵 Trie
- KMP 的思想:对 Trie 树上所有的结点构造失配指针
- 多模式字符串 匹配算法
- KMP算法的树形扩展
一般KMP只能处理一个patten, 而结合了Trie树就可以 多个patten 查找
- 节点存储信息
{child[26], fail指针, vector<int> exist }
- fail指针
i->fail = j
失配指针- KMP 只对一个模式串做匹配,而 AC 自动机要对多个模式串做匹配。有可能 fail 指针指向的结点对应着另一个模式串,两者前缀不同
- 表示
word[j]
是word[i]
的最长后缀状态 - 在一个分支上走不下去了之后,我们不必头铁,可以换个方向继续往下走,而fail数组的作用就是这样,当发生失配时,我们可以通过预处理出的fail数组告诉我们其他分支的信息 (即满足当前字符串最长后缀的节点),然后继续向后遍历即可
- 利用fail指针来补充trie树的信息,使其成为一个图,然后在匹配过程中,我们无需考虑fail指针,直接令 j = t r [ j ] [ t ] j = tr[j][t] j=tr[j][t],这里其实就包括了 t + ′ a ′ t+'a' t+′a′这个字符如果不存在时的情况,如果不存在,其实就等价于 j = t r [ f a i l [ j ] ] [ t ] j = tr[fail[j]][t] j=tr[fail[j]][t]
- exist列表 表示当前节点为止记录上的单词长度 序列
- 代码 模板
- 记录 所有匹配上的 字符串 的总数
#pragma warning(disable:4996)
#include <iostream>
#include <cstring>
#include <string>
#include <cmath>
#include <cstdio>
#include <stdio.h>
#include <cstdlib>
#include <algorithm>
#include <vector>
#include <set>
#include <map>
#include <iomanip>
#define rep(i,a,b) for(int i = a; i <= b ; i ++ )
#define pre(i,a,b) for(int i = b ; i >= a ; i --)
#define ll long long
#define inf 0x3f3f3f3f
#define ull unsigned long long
#define ios ios::sync_with_stdio(false),cin.tie(0)
using namespace std;
typedef pair<int, int> PII;
const int N = 10010, S = 55, M = 1000010;
int n;
int tr[N * S][26], cnt[N * S], idx; // 节点编号 长度
char str[M]; // 字符串
int q[N * S], ne[N * S]; // BFS队列 fail指针
void insert() {
int p = 0;
for (int i = 0; str[i]; i++) {
int t = str[i] - 'a';
if (!tr[p][t]) tr[p][t] = ++idx;
p = tr[p][t];
}
cnt[p]++;
}
void build() {
int hh = 0, tt = -1;
for (int i = 0; i < 26; i++)
if (tr[0][i])
q[++tt] = tr[0][i];
while (hh <= tt) {
int t = q[hh++];
for (int i = 0; i < 26; i++) {
int p = tr[t][i];
if (!p) tr[t][i] = tr[ne[t]][i];
else {
ne[p] = tr[ne[t]][i];
q[++tt] = p;
}
}
}
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
memset(tr, 0, sizeof tr);
memset(cnt, 0, sizeof cnt);
memset(ne, 0, sizeof ne);
idx = 0;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%s", str);
insert();
}
打印树结构
//for (int i = 0; i <= 5; i++) {
// for (int j = 0; j < 26; j++) {
// if (tr[i][j]) {
// cout << "Node " << i << " have "
// << char(j + 'a') << " is #" << tr[i][j] << endl;
// }
// }
//}
//cout << endl;
//build();
打印树结构 其实我还不知道为啥要改变树的结构!
//for (int i = 0; i <= 5; i++) {
// for (int j = 0; j < 26; j++) {
// if (tr[i][j]) {
// cout << "Node " << i << " have "
// << char(j + 'a') << " is #" << tr[i][j] << endl;
// }
// }
//}
scanf("%s", str);
int res = 0;
for (int i = 0, j = 0; str[i]; i++) {
int t = str[i] - 'a';
j = tr[j][t];
int p = j;
while (p) {
res += cnt[p];
//cnt[p] = 0; // 只统计一次
p = ne[p]; // 不漏解
}
}
printf("%d\n", res);
}
return 0;
}
后缀数组:Suffix Array
- 定义:由两个数组组成:
- s a [ i ] s a[i] sa[i] 表示将所有后缀排序后第 i i i 小的后缀的编号
- r k [ i ] r k[i] rk[i] 表示后缀 i i i 的排名
- 性质: s a [ r k [ i ] ] = r k [ s a [ i ] ] = i s a[r k[i]]=r k[s a[i]]=i sa[rk[i]]=rk[sa[i]]=i
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 1000010;
char s[N];
int n, w, sa[N], rk[N << 1], oldrk[N << 1];
// 为了防止访问 rk[i+w] 导致数组越界,开两倍数组。
// 当然也可以在访问前判断是否越界,但直接开两倍数组方便一些。
int main() {
int i, p;
scanf("%s", s + 1);
n = strlen(s + 1);
for (i = 1; i <= n; ++i) sa[i] = i, rk[i] = s[i];
for (w = 1; w < n; w <<= 1) {
sort(sa + 1, sa + n + 1, [](int x, int y) {
return rk[x] == rk[y] ? rk[x + w] < rk[y + w] : rk[x] < rk[y];
}); // 这里用到了 lambda
memcpy(oldrk, rk, sizeof(rk));
// 由于计算 rk 的时候原来的 rk 会被覆盖,要先复制一份
for (p = 0, i = 1; i <= n; ++i) {
if (oldrk[sa[i]] == oldrk[sa[i - 1]] &&
oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]) {
rk[sa[i]] = p;
} else {
rk[sa[i]] = ++p;
} // 若两个子串相同,它们对应的 rk 也需要相同,所以要去重
}
}
for (i = 1; i <= n; ++i) printf("%d ", sa[i]);
return 0;
}
- 寻找最小的循环移动位置,将字符串S复制一份变成SS就转化成了后缀排序问题
- 在字符串中找子串:构造T的后缀数组,如果S在T中出现过,那么必定是一些后缀的前缀,二分查找!
- 从字符串首尾取字符最小化字典序
后缀自动机 Suffix Automaton
- 处理字符串后缀
- 集训队论文
- 陈立杰ppt
广义后缀自动机
后缀树
回文树
Manacher
- 求字符串的 最长回文子串
- 把一般意义上的 n方 复杂度算法,提升为 O(n)复杂度
动态规划 O(n^2)
vector<int> d1(n), d2(n);
// 分别表示以位置 为中心的长度为奇数和长度为偶数的回文串个数
for (int i = 0; i < n; i++) {
d1[i] = 1;
while (0 <= i - d1[i] && i + d1[i] < n && s[i - d1[i]] == s[i + d1[i]]) {
d1[i]++;
}
d2[i] = 0;
while (0 <= i - d2[i] - 1 && i + d2[i] < n &&
s[i - d2[i] - 1] == s[i + d2[i]]) {
d2[i]++;
}
}
马拉车算法 线性
char s_n[5000]; // &1#2^ 处理后的字符串
int p[5000]; // p[i]的i位置的回文串的半径
string longestPalindrome(string s) {
string res = "";
manacher(s, res);
return res;
}
// 修改原字符串到 字符串长度变为奇数
int init(string& str) {
int len = str.size();
s_n[0] = '$';
s_n[1] = '#';
int j = 2;
for (int i = 0; i < len; i++) {
s_n[j++] = str[i];
s_n[j++] = '#';
}
s_n[j++] = '^';
s_n[j] = '\0'; // 终止符加上
return j;
}
int manacher(string& str, string& res) {
int len = init(str);
int max_len = -1;
// mx:以id为中心的最长回文的右边界
// mx = id + p[id]
// p[i]: 以i为中心的最长回文半径(包括中心字符)
int id = 0, mx = 0;
int final_id = 0;
res = "";
// 求解位置和长度
for (int i = 1; i < len; i++) {
if (i < mx) p[i] = min(p[2 * id - 1], mx - i);
else p[i] = 1;
while (s_n[i - p[i]] == s_n[i + p[i]]) p[i]++;
if (mx < i + p[i]) {
id = i;
mx = i + p[i];
}
if (p[i] - 1 > max_len) {
max_len = p[i] - 1; // 最长回文串的长度
final_id = i; // 最长回文串的位置(修改后的字符串)
}
}
// 获取回文串
int left = max(0, final_id - max_len);
int right = min(len, final_id + max_len);
for (int i = left; i <= right; i++) {
if (s_n[i] != '#' && s_n[i] != '^' && s_n[i] != '$')
res += s_n[i];
}
return max_len;
}
回文分割
- 最近流行
字符串分割成由回文子串构成的序列
class Solution {
public:
vector<vector<string>> partition(string s) {
res.clear();
int N = s.size();
dp.resize(N, vector<int>(N, false));
init(s);
dfs(0, s);
return res;
}
private:
vector<vector<int>> dp;
vector<vector<string>> res;
vector<string> path;
void init(string& str) {
int N = str.size();
for (int j = 0; j < N; j++) {
for (int i = 0; i <= j; i++) {
if (i == j) dp[i][j] = true;
else if (str[i] == str[j] && (j - i < 2 || dp[i + 1][j - 1]))
dp[i][j] = true;
}
}
}
void dfs(int pos, string& str) {
int N = str.size();
if (pos == N) {
res.push_back(path);
return;
}
for (int i = pos; i < N; i++) {
if (dp[pos][i]) {
path.push_back(str.substr(pos, i - pos + 1));
dfs(i + 1, str);
path.pop_back();
}
}
}
// bool checkPalindrome(string& str, int left, int right) {
// if (left > right) return false;
// while (left < right) {
// if (str[left++] != str[right--])
// return false;
// }
// return true;
// }
};
字符串分割成回文串序列的最少分割次数
class Solution {
public:
int minCut(string s) {
int N = s.size();
init(s);
dp2.resize(N + 1, INT_MAX); dp2[0] = 0;
for (int right = 1; right <= N; right++) {
for (int left = 1; left <= right; left++) {
if (dp1[left - 1][right - 1])
dp2[right] = min(dp2[right], dp2[left - 1] + 1);
}
}
return dp2[N] - 1;
}
private:
vector<vector<bool>> dp1; // 前一个DP判断区间是不是回文
vector<int> dp2; // 后一个DP计算回文串个数
void init(string& str) {
int N = str.size();
dp1.resize(N, vector<bool>(N, false));
for (int right = 0; right < N; right++) {
for (int left = 0; left <= right; left++) {
if (left == right) dp1[left][right] = true;
else if (str[left] == str[right] &&
(right - left < 2 || dp1[left + 1][right - 1])) {
dp1[left][right] = true;
}
}
}
}
};
序列自动机
最小表示法
- Finding the smallest cyclic shift
- 解决 字符串 最小表示问题 的 方法
- 循环同构:
- S 和 T 循环同构 ==> S [ i : n ] + S [ 1 : i − 1 ] = T S[i : n] + S[1:i-1] = T S[i:n]+S[1:i−1]=T
- S的最小表示:和 S 循环同构 的所有 字符串中 字典序最小 的字符串!
int k = 0, i = 0, j = 1;
while (k < n && i < n && j < n) {
if (sec[(i + k) % n] == sec[(j + k) % n]) {
k++;
} else {
sec[(i + k) % n] > sec[(j + k) % n] ? i = i + k + 1 : j = j + k + 1; // 第i + k 处不满足字符相等 就前k个字符都得跳过!
if (i == j) i++; // 否则随意 加位置
k = 0;
}
}
i = min(i, j); // i 和 j 较小值为答案!
Lyndon 分解
- Lyndon串:若字符串S满足其本身的字典序严格小于其任何一个真后缀,则称S是 Lyndon 串
- 等价于 S 本身是所有 S 的循环同构串中字典序唯一最小的一个串
- Lyndon 分解:串
s
s
s 的 Lyndon 分解记为
- s = w 1 w 2 ⋯ w k s=w_{1} w_{2} \cdots w_{k} s=w1w2⋯wk, 其中所有 w i w_{i} wi 为简单串
- 并且他们 的字典序按照非严格单减排序,即 w 1 ≥ w 2 ≥ ⋯ ≥ w k w_{1} \geq w_{2} \geq \cdots \geq w_{k} w1≥w2≥⋯≥wk 。这样的分解存在且唯一!
Duval 算法
- 如果一个字符串 t t t 能够分解为 t = w w ⋯ w ˉ t=w w \cdots \bar{w} t=ww⋯wˉ 的形式
- 其中 w w w 是一 个 Lyndon 串,而 w ˉ \bar{w} wˉ 是 w w w 的前缀 ( w ˉ (\bar{w} (wˉ 可能是空串 ) , ), ), 那么称 t t t 是近似简单串 ( pre-simple ) 或者 近似 Lyndon 串
- Lyndon 串也是 近似 Lyndon 串
- Duval 算法运用了贪心的思想。算法过程中我们把串 s s s 分成三个部分 s = s 1 s 2 s 3 , s=s_{1} s_{2} s_{3}, s=s1s2s3, 其中 s 1 s_{1} s1 是一 个 Lyndon 串,它的 Lyndon 分解已经记录 ; s 2 s_{2} s2 是一个近似 Lyndon 串 ; s 3 s_{3} s3 是未处理的部分。
- 整体描述一下,该算法每一次尝试将 s 3 s_{3} s3 的首字符添加到 s 2 s_{2} s2 的末尾。如果 s 2 s_{2} s2 不再是近似 Lyndon 串,那么我们就可以将 s 2 s_{2} s2 截出一部分前缀 ( 即 Lyndon 分解 ) ) ) 接在 s 1 s_{1} s1 末尾。
// duval_algorithm
vector<string> duval(string const& s) {
int n = s.size(), i = 0;
vector<string> factorization;
while (i < n) {
int j = i + 1, k = i;
while (j < n && s[k] <= s[j]) {
if (s[k] < s[j])
k = i;
else
k++;
j++;
}
while (i <= k) {
factorization.push_back(s.substr(i, j - k));
i += j - k;
}
}
return factorization;
}
- 可用于求解 最小表示法 问题!
- 在分解的过程中记录每一次的近似 Lyndon 串的开头即可
// smallest_cyclic_string
string min_cyclic_string(string s) {
s += s;
int n = s.size();
int i = 0, ans = 0;
while (i < n / 2) {
ans = i;
int j = i + 1, k = i;
while (j < n && s[k] <= s[j]) {
if (s[k] < s[j])
k = i;
else
k++;
j++;
}
while (i <= k) i += j - k;
}
return s.substr(ans, n / 2);
}
图论
图的存储
常见的
- 直接存边:由于直接存边的遍历效率低下,一般不用于遍历图
struct Edge {
int u, v; // 起点 终点
int w; // 权重
};
vector<Edge> e;
- 邻接矩阵:邻接矩阵只适用于没有重边(或重边可以忽略)的情况
vector<vector<bool> > adj;
- 邻接表:支持动态增加元素的数据结构构成的数组
vector<vector<int> > adj;
unordered_map<int, pair<int, int>> adj;
链式前向星:用链表实现的邻接表
- 以储存边的方式来存储图
- 读入每条边的信息,将边存放在数组中,把数组中的边按照起点顺序排序(可以使用基数排序,如下面例程),前向星就构造完了
- 通常用在点的数目太多,或两点之间有多条弧的时候
- 一般在别的数据结构不能使用的时候才考虑用前向星。除了不能直接用起点终点定位以外,前向星几乎是完美的!
#include <iostream>
#include <vector>
using namespace std;
int n, m;
vector<bool> vis;
vector<int> head, nxt, to;
void add(int u, int v) {
// 表示与第i条边同起点的下一条边的存储位置
nxt.push_back(head[u]); // 边 - 边
// 表示以i为起点的第一条边存储的位置
head[u] = to.size(); // 点 - 边
// 表示第i条边的终点
to.push_back(v); // 边 - 点
}
bool find_edge(int u, int v) {
for (int i = head[u]; ~i; i = nxt[i]) { // ~i 表示 i != -1
if (to[i] == v) {
return true;
}
}
return false;
}
void dfs(int u) {
if (vis[u]) return;
vis[u] = true;
for (int i = head[u]; ~i; i = nxt[i]) dfs(to[i]);
}
int main() {
cin >> n >> m;
vis.resize(n + 1, false);
head.resize(n + 1, -1);
for (int i = 1; i <= m; ++i) {
int u, v;
cin >> u >> v;
add(u, v);
}
return 0;
}
- 存各种图都很适合,但不能快速查询一条边是否存在,也不能方便地对一个点的出边进行排序。
- 优点是边是带编号的,有时会非常有用,而且如果 cnt 的初始值为奇数,存双向边时 i ^ 1 即是 i 的反边(常用于 网络流 )
单源最短路
- Dijkstra,spfa
多源最短路
- Floy
最小生成树
- Kruskal,Prim
拓扑排序
- 课程表ii 入度
class Solution {
public:
bool topSort(vector<vector<int>>& G, vector<int>& O, vector<int>& D) {
int N = D.size(), idx = 0;
queue<int> Q;
for (int i = 0; i < N; i++)
if (D[i] == 0) Q.push(i);
while (!Q.empty()) {
int X = Q.front(); Q.pop();
O[idx++] = X;
for (int i = 0; i < G[X].size(); i++) {
int x = G[X][i]; D[x]--;
if (D[x] == 0) Q.push(x);
}
G[X].clear();
}
if (idx == N) return true;
return false;
}
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
vector<int> inD(numCourses), res(numCourses);
vector<vector<int>> G(numCourses);
for (int i = 0; i < prerequisites.size(); i++) {
G[prerequisites[i][1]].push_back(prerequisites[i][0]);
inD[prerequisites[i][0]]++;
}
if (topSort(G, res, inD)) return res;
return {};
}
};
- 矩阵中的递增路径 出度
class Solution {
public:
int longestIncreasingPath(vector<vector<int>>& matrix) {
if (matrix.empty() || matrix[0].empty()) return 0;
H = matrix.size(); W = matrix[0].size();
vector<vector<int>> outDgree(H, vector<int>(W, 0));
for (int i = 0; i < H; i++) {
for (int j = 0; j < W; j++) {
for (int k = 0; k < 4; k++) {
int i1 = i + dirs[k][0], j1 = j + dirs[k][1];
if (i1 < 0 || i1 >= H || j1 < 0 || j1 >= W) continue;
if (matrix[i1][j1] <= matrix[i][j]) continue;
++outDgree[i][j]; // 出度
}
}
}
queue<pair<int, int>> Q;
for (int i = 0; i < H; i++) {
for (int j = 0; j < W; j++) {
if (outDgree[i][j] == 0)
Q.push(make_pair(i, j)); // 从出度为0为边界条件 反向更新所有可达点
}
}
int ans = 0;
while (!Q.empty()) {
++ans;
int sz = Q.size();
for (int i = 0; i < sz; i++) {
auto X = Q.front(); Q.pop();
int y = X.first, x = X.second;
for (int k = 0; k < 4; k++) {
int x1 = x + dirs[k][1], y1 = y + dirs[k][0];
if (x1 < 0 || x1 >= W || y1 < 0 || y1 >= H) continue;
if (matrix[y][x] <= matrix[y1][x1]) continue;
--outDgree[y1][x1]; // (y1, x1) -> (y, x)
if (outDgree[y1][x1] == 0) Q.push(make_pair(y1, x1));
}
}
}
return ans;
}
private:
int dirs[4][2] = {{-1, 0}, {1, 0}, {0, 1}, {0, -1}};
int H, W;
};
最大流,最小费用流
- Dinic,zkw
匈牙利算法
- KM(网络流TLE后,用这个)
欧拉回路
Tarjan
- 强连通,边点双联通,割点,桥
2-SAT
- 有很多个集合,每个集合里面有若干元素,现给出一些取元素的规则,要你判断是否可行,可行则给出一个可行方案。如果所有集合中,元素个数最多的集合有k个,那么我们就说这是一个k-sat问题,同理,2-sat问题就是k=2时的情况
- k > 2 NP完全问题
- 而k == 2,存在多项式解
动态规划
背包问题(背包九讲)
- 01背包,分组背包,完全背包,树上背包
记忆化搜索
- dfs + bfs
数位dp
- 小于N有多少怎样怎样的数字,N很大
状态压缩dp
- 通常用于各种集合问题
- 位运算 + DP
树形dp
概率dp
- 不知道大家学过随机过程没
- 概率 DP 用于解决概率问题与期望问题
dp优化
- 斜率优化,单调队列优化,四边形不等式优化
数学
组合数学
- 离散 / 具体 数学,数论、排列组合、概率期望、多项式 等!
- 求C(n,m),求斯特林数,卡特兰数,各种混着dp,容斥,鸽笼
基本操作
位运算
- 高效地进行某些运算,代替其它低效的方式。
- 表示集合。(常用于 状压 DP )
- 题目本来就要求进行位运算
- 一些替换有用的操作 OI -wiki
快速幂
- ( a b ) % m (a ^ b) \% m (ab)%m
long long binpow(long long a, long long b, long long m) {
a %= m;
long long res = 1;
while (b > 0) {
if (b & 1) res = res * a % m;
a = a * a % m;
b >>= 1;
}
return res;
}
进制转换
- 二进制 + 八进制 + 十六进制
高精度计算 / bignum计算
- Arbitrary-Precision Arithmetic
- 常用 字符串 表示 字符串的 表示!
入门:加减乘除
#include <cstdio>
#include <cstring>
static const int LEN = 1004;
int a[LEN], b[LEN], c[LEN], d[LEN];
void clear(int a[]) {
for (int i = 0; i < LEN; ++i) a[i] = 0;
}
void read(int a[]) {
static char s[LEN + 1];
scanf("%s", s);
clear(a);
int len = strlen(s);
for (int i = 0; i < len; ++i) a[len - i - 1] = s[i] - '0';
}
void print(int a[]) {
int i;
for (i = LEN - 1; i >= 1; --i)
if (a[i] != 0) break;
for (; i >= 0; --i) putchar(a[i] + '0');
putchar('\n');
}
void add(int a[], int b[], int c[]) {
clear(c);
for (int i = 0; i < LEN - 1; ++i) {
c[i] += a[i] + b[i];
if (c[i] >= 10) {
c[i + 1] += 1;
c[i] -= 10;
}
}
}
void sub(int a[], int b[], int c[]) {
clear(c);
for (int i = 0; i < LEN - 1; ++i) {
c[i] += a[i] - b[i];
if (c[i] < 0) {
c[i + 1] -= 1;
c[i] += 10;
}
}
}
void mul(int a[], int b[], int c[]) {
clear(c);
for (int i = 0; i < LEN - 1; ++i) {
for (int j = 0; j <= i; ++j) c[i] += a[j] * b[i - j];
if (c[i] >= 10) {
c[i + 1] += c[i] / 10;
c[i] %= 10;
}
}
}
inline bool greater_eq(int a[], int b[], int last_dg, int len) {
if (a[last_dg + len] != 0) return true;
for (int i = len - 1; i >= 0; --i) {
if (a[last_dg + i] > b[i]) return true;
if (a[last_dg + i] < b[i]) return false;
}
return true;
}
void div(int a[], int b[], int c[], int d[]) {
clear(c);
clear(d);
int la, lb;
for (la = LEN - 1; la > 0; --la)
if (a[la - 1] != 0) break;
for (lb = LEN - 1; lb > 0; --lb)
if (b[lb - 1] != 0) break;
if (lb == 0) {
puts("> <");
return;
}
for (int i = 0; i < la; ++i) d[i] = a[i];
for (int i = la - lb; i >= 0; --i) {
while (greater_eq(d, b, i, lb)) {
for (int j = 0; j < lb; ++j) {
d[i + j] -= b[j];
if (d[i + j] < 0) {
d[i + j + 1] -= 1;
d[i + j] += 10;
}
}
c[i] += 1;
}
}
}
int main() {
read(a);
char op[4];
scanf("%s", op);
read(b);
switch (op[0]) {
case '+':
add(a, b, c);
print(c);
break;
case '-':
sub(a, b, c);
print(c);
break;
case '*':
mul(a, b, c);
print(c);
break;
case '/':
div(a, b, c, d);
print(c);
print(d);
break;
default:
puts("> <");
}
return 0;
}
压位 高精度
- 不用字符串里面 每个位 进行 分解,用于计算
- 可以两位 两位 的计算,比如 按照 100进制进行 分解
- 以加法为例
//这里的 a,b,c 数组均为 p 进制下的数
//最终输出答案时需要将数字转为十进制
void add(int a[], int b[], int c[]) {
clear(c);
for (int i = 0; i < LEN - 1; ++i) {
c[i] += a[i] + b[i];
if (c[i] >= p) { //在普通高精度运算下,p=10
c[i + 1] += 1;
c[i] -= p;
}
}
}
Karatsuba 乘法
- 分治思想 用于 高精度 乘法!
int *karatsuba_polymul(int n, int *a, int *b) {
if (n <= 32) {
// 规模较小时直接计算,避免继续递归带来的效率损失
int *r = new int[n * 2 + 1]();
for (int i = 0; i <= n; ++i)
for (int j = 0; j <= n; ++j) r[i + j] += a[i] * b[j];
return r;
}
int m = n / 2 + 1;
int *r = new int[m * 4 + 1]();
int *z0, *z1, *z2;
z0 = karatsuba_polymul(m - 1, a, b);
z2 = karatsuba_polymul(n - m, a + m, b + m);
// 计算 z1
// 临时更改,计算完毕后恢复
for (int i = 0; i + m <= n; ++i) a[i] += a[i + m];
for (int i = 0; i + m <= n; ++i) b[i] += b[i + m];
z1 = karatsuba_polymul(m - 1, a, b);
for (int i = 0; i + m <= n; ++i) a[i] -= a[i + m];
for (int i = 0; i + m <= n; ++i) b[i] -= b[i + m];
for (int i = 0; i <= (m - 1) * 2; ++i) z1[i] -= z0[i];
for (int i = 0; i <= (n - m) * 2; ++i) z1[i] -= z2[i];
// 由 z0、z1、z2 组合获得结果
for (int i = 0; i <= (m - 1) * 2; ++i) r[i] += z0[i];
for (int i = 0; i <= (m - 1) * 2; ++i) r[i + m] += z1[i];
for (int i = 0; i <= (n - m) * 2; ++i) r[i + m * 2] += z2[i];
delete[] z0;
delete[] z1;
delete[] z2;
return r;
}
void karatsuba_mul(int a[], int b[], int c[]) {
int *r = karatsuba_polymul(LEN - 1, a, b);
memcpy(c, r, sizeof(int) * LEN);
for (int i = 0; i < LEN - 1; ++i)
if (c[i] >= 10) {
c[i + 1] += c[i] / 10;
c[i] %= 10;
}
delete[] r;
}
- 这种方法,会有整数溢出 问题!
高精度整数类 封装
#define MAXN 9999
// MAXN 是一位中最大的数字
#define MAXSIZE 10024
// MAXSIZE 是位数
#define DLEN 4
// DLEN 记录压几位
struct Big {
int a[MAXSIZE], len;
bool flag; // 标记符号'-'
Big() {
len = 1;
memset(a, 0, sizeof a);
flag = 0;
}
Big(const int);
Big(const char*);
Big(const Big&);
Big& operator=(const Big&);
Big operator+(const Big&) const;
Big operator-(const Big&) const;
Big operator*(const Big&)const;
Big operator/(const int&) const;
// TODO: Big / Big;
Big operator^(const int&) const;
// TODO: Big ^ Big;
// TODO: Big 位运算;
int operator%(const int&) const;
// TODO: Big ^ Big;
bool operator<(const Big&) const;
bool operator<(const int& t) const;
inline void print() const;
};
Big::Big(const int b) {
int c, d = b;
len = 0;
// memset(a,0,sizeof a);
CLR(a);
while (d > MAXN) {
c = d - (d / (MAXN + 1) * (MAXN + 1));
d = d / (MAXN + 1);
a[len++] = c;
}
a[len++] = d;
}
Big::Big(const char* s) {
int t, k, index, l;
CLR(a);
l = strlen(s);
len = l / DLEN;
if (l % DLEN) ++len;
index = 0;
for (int i = l - 1; i >= 0; i -= DLEN) {
t = 0;
k = i - DLEN + 1;
if (k < 0) k = 0;
g(j, k, i) t = t * 10 + s[j] - '0';
a[index++] = t;
}
}
Big::Big(const Big& T) : len(T.len) {
CLR(a);
f(i, 0, len) a[i] = T.a[i];
// TODO:重载此处?
}
Big& Big::operator=(const Big& T) {
CLR(a);
len = T.len;
f(i, 0, len) a[i] = T.a[i];
return *this;
}
Big Big::operator+(const Big& T) const {
Big t(*this);
int big = len;
if (T.len > len) big = T.len;
f(i, 0, big) {
t.a[i] += T.a[i];
if (t.a[i] > MAXN) {
++t.a[i + 1];
t.a[i] -= MAXN + 1;
}
}
if (t.a[big])
t.len = big + 1;
else
t.len = big;
return t;
}
Big Big::operator-(const Big& T) const {
int big;
bool ctf;
Big t1, t2;
if (*this < T) {
t1 = T;
t2 = *this;
ctf = 1;
} else {
t1 = *this;
t2 = T;
ctf = 0;
}
big = t1.len;
int j = 0;
f(i, 0, big) {
if (t1.a[i] < t2.a[i]) {
j = i + 1;
while (t1.a[j] == 0) ++j;
--t1.a[j--];
// WTF?
while (j > i) t1.a[j--] += MAXN;
t1.a[i] += MAXN + 1 - t2.a[i];
} else
t1.a[i] -= t2.a[i];
}
t1.len = big;
while (t1.len > 1 && t1.a[t1.len - 1] == 0) {
--t1.len;
--big;
}
if (ctf) t1.a[big - 1] = -t1.a[big - 1];
return t1;
}
Big Big::operator*(const Big& T) const {
Big res;
int up;
int te, tee;
f(i, 0, len) {
up = 0;
f(j, 0, T.len) {
te = a[i] * T.a[j] + res.a[i + j] + up;
if (te > MAXN) {
tee = te - te / (MAXN + 1) * (MAXN + 1);
up = te / (MAXN + 1);
res.a[i + j] = tee;
} else {
up = 0;
res.a[i + j] = te;
}
}
if (up) res.a[i + T.len] = up;
}
res.len = len + T.len;
while (res.len > 1 && res.a[res.len - 1] == 0) --res.len;
return res;
}
Big Big::operator/(const int& b) const {
Big res;
int down = 0;
gd(i, len - 1, 0) {
res.a[i] = (a[i] + down * (MAXN + 1) / b);
down = a[i] + down * (MAXN + 1) - res.a[i] * b;
}
res.len = len;
while (res.len > 1 && res.a[res.len - 1] == 0) --res.len;
return res;
}
int Big::operator%(const int& b) const {
int d = 0;
gd(i, len - 1, 0) d = (d * (MAXN + 1) % b + a[i]) % b;
return d;
}
Big Big::operator^(const int& n) const {
Big t(n), res(1);
int y = n;
while (y) {
if (y & 1) res = res * t;
t = t * t;
y >>= 1;
}
return res;
}
bool Big::operator<(const Big& T) const {
int ln;
if (len < T.len) return 233;
if (len == T.len) {
ln = len - 1;
while (ln >= 0 && a[ln] == T.a[ln]) --ln;
if (ln >= 0 && a[ln] < T.a[ln]) return 233;
return 0;
}
return 0;
}
inline bool Big::operator<(const int& t) const {
Big tee(t);
return *this < tee;
}
inline void Big::print() const {
printf("%d", a[len - 1]);
gd(i, len - 2, 0) { printf("%04d", a[i]); }
}
inline void print(Big s) {
// s不要是引用,要不然你怎么print(a * b);
int len = s.len;
printf("%d", s.a[len - 1]);
gd(i, len - 2, 0) { printf("%04d", s.a[i]); }
}
char s[100024];
平衡三进制
- 0 , 1 , 2 > − 1 , 0 , 1 > z , 0 , 1 0,1,2 > -1,0,1 > z,0,1 0,1,2>−1,0,1>z,0,1
博弈论
- Nim游戏,SG函数(其余靠猜)
数论
- 欧几里得算法(GCD),扩展欧几里得,欧拉筛,欧拉函数,莫比乌斯反演(百度贾志鹏线性筛),中国剩余定理
几何
- 《挑战程序设计》最新版
- 上海交大acm的《算法与实现》
- 刘汝佳的《算法竞赛,入门经典》)(注意精度)
高数
- 高斯消元
- 数值积分