杂七杂八の学习笔记
动态开点线段树
动态开点线段树就是在普通的线段树基础上再去维护每个点的左右儿子的节点编号,在进行更新时一旦遇到空节点就直接新建一个即可。查询是遇到空节点直接返回0
权值线段树
和普通线段树不同的是,权值线段树存储的时对应的值出现次数
举个例子,有一个长度为
\(10的序列{1,5,2,3,4,1,3,4,4,4}\)。
那么统计每个数出现的次数。易知
\(1
出现了
2
次,
2
出现了
1
次,
3
出现了
2
次,
4
出现了
4
次,
5
出现了
1
次。\)
那么我们可以建出一棵这样的权值线段树:
也就是说每个叶子节点代表了每个数的出现次数。
空间复杂度上限大概是\(O(4\omega)\),\(\omega\)代表值域长度,但是如果一旦值域很大我们就比较难受了,那咋办啊,哎,我们可以用动态开点线段树啊,这样不就完美解决了吗,这样空间复杂度就降为了\(O(nlog\omega)\)
看几道例题
考虑到每个数产生的贡献为在他前边且比它小的数的个数乘上在他后边比他大的数,那我们就可以拿权值线段树了,从前往后扫一遍,从后往前扫一遍即可
Code()
#include <bits/stdc++.h>
using namespace std ;
typedef long long ll ;
const ll MAXN = 3e4 + 10 ;
const ll lim = 1e5 ;
ll a[MAXN], ans[MAXN] ;
ll t[MAXN << 2], ls[MAXN << 2], rs[MAXN << 2], cnt, root ;
inline void update (ll &now, ll l, ll r, ll x, ll k) {
if (!now) now = ++cnt ;
if (l == r) {
t[now] += k ;
return ;
}
ll mid = (l + r) / 2 ;
if (x <= mid) update(ls[now], l, mid, x, k) ;
else update(rs[now], mid + 1, r, x, k) ;
t[now] = t[ls[now]] + t[rs[now]] ;
}
inline ll query1 (ll now, ll l, ll r, ll x, ll y) {
if (!now) return 0 ;
if (r < y) return t[now] ;
ll mid = (l + r) / 2, sum = 0 ;
if (x <= mid) sum += query1(ls[now], l, mid, x, y) ;
if (y > mid) sum += query1(rs[now], mid + 1, r, x, y) ;
return sum ;
}
inline ll query2 (ll now, ll l, ll r, ll x, ll y) {
if (!now) return 0 ;
if (l > x) return t[now] ;
ll mid = (l + r) / 2, sum = 0 ;
if (x <= mid) sum += query2(ls[now], l, mid, x, y) ;
if (y > mid) sum += query2(rs[now], mid + 1, r, x, y) ;
return sum ;
}
int main () {
ll n ;
cin >> n ;
for (ll i = 1 ; i <= n ; i++) {
cin >> a[i] ;
update(root, 1, lim, a[i], 1) ;
ans[i] = query1(1, 1, lim, 1, a[i]) ;
// cout << ans[i] << '\n' ;
}
memset(t, 0, sizeof(t)) ;
ll sum = 0 ;
for (ll i = n ; i >= 1 ; i--) {
update(root, 1, lim, a[i], 1) ;
ans[i] = ans[i] * query2(1, 1, lim, a[i], lim) ;
sum += ans[i] ;
}
cout << sum << '\n' ;
}
dsu on tree
用来统计子树内的信息(离线)
大致步骤:
1.先遍历非重儿子,统计答案,不保留对统计答案数组的影响
2.遍历重儿子,统计答案,保留对统计答案数组的影响
3.遍历非重儿子加入节点贡献
费用流
在网络流的基础上每条边加上了单位流量的费用,求在达到最大流的前提下的最小费用
考虑在原来算法的基础进行操作,我们可以改变每次的增广路,每次找到费用和最小的增广路,进行增广即可,用spfa寻找就行
inline bool bfs() {
for (int i = 1 ; i <= n ; i++) vis[i] = 0, dis[i] = inf, cur[i] = head[i] ;
queue<int> q ;
q.push(s) ;
vis[s] = 1 ;
dis[s] = 0 ;
while (!q.empty()) {
int x = q.front() ;
q.pop() ;
vis[x] = 0 ;
for (int i = head[x] ; i ; i = e[i].nxt) {
int v = e[i].to, w2 = e[i].w2 ;
if (!e[i].w1 || dis[v] <= dis[x] + w2) continue ;
dis[v] = dis[x] + w2 ;
if (!vis[v]) q.push(v), vis[v] = 1 ;
}
}
return dis[t] != inf ;
}
inline int dfs (int x, int l) {
if (x == t) return l ;
int f = 0 ;
vis[x] = 1 ;
for (int i = cur[x] ; i ; i = e[i].nxt) {
int v = e[i].to, w2 = e[i].w2 ;
cur[x] = i ;
if (!e[i].w1 || dis[v] < dis[x] + w2 || vis[v]) continue ;
int nowf = dfs(v, min(e[i].w1, l - f)) ;
if (!nowf) vis[v] = -1 ;
else {
f += nowf ;
e[i].w1 -= nowf ;
e[i ^ 1].w1 += nowf ;
sum += nowf * w2 ;
}
}
vis[x] = 0 ;
return f ;
}
CDQ分治
用于解决类似 \(a_j \le a_i\) 且 \(b_j\le b_i\) 且 \(c_j \le c_i\) 的一种算法
具体实现:
就和名字一样,分治。我们先来考虑如果去掉一个条件会不会好做一点呢?确实是会的,那么我么考虑怎么去掉一个条件,我们直接先按 \(a\) 为关键字排序,假设我们现在已经处理完了 \(work(l, mid)\) 和 \(work(mid + 1,r)\) ,那么我们现在只需考虑跨区间的合法数量,设 \(l \le i \le mid, mid + 1\le j \le r\),那么一定有 \(a_i \le a_j\),这是因为已经按 \(a\) 为关键字排序过了,所以我们不需要考虑第一个限制了,方便起见,我们将区间 \((l,mid)\) 以及 \((mid + 1,r)\)分别按 \(b\) 为关键字排序,然后对 \(j\) 进行枚举, 我们对于每个 \(b_i\le b_j\) 的 \(i\) 的 \(c_i\) 插入到权值线段树中,但如果直接去暴力枚举这个 \(i\) 复杂度会爆,但我们可以使用双指针啊!因为已经按 \(b\) 排序过了,之后我们直接在权值线段树中有多少个 数小于等于 \(c_j\) 即可
左偏树
感觉这个东西和treap挺像的,定义一个外界点为子节点数小于2的节点,定义每个节点的\(dist\)为其子树内与其距离最近的外界点之间的边数,为什么交左偏树呢,因为每个节点的左儿子的\(dist\)都要不小于右儿子的\(dist\),故名“左偏树”,又这条性质可知\(dist_{节点} = dist_{右儿子} + 1\),知道这条性质后,我们就可以进行一些操作了。
合并(\(merge\))
由于合并要满足堆的性质(先来说小根堆,大根堆同理),于是我们先将两个堆的堆顶进行比较,然后让小的一个作为堆顶,小的那个堆的左儿子作为合并后的左儿子,右儿子与另外一个堆进行递归合并,知道一方为空,为了满足左偏树的性质如果\(dist_{左儿子}\)小于\(dist_{右儿子}\),那么左右儿子交换即可。大概就这么写
inline int merge(int x, int y) {
if (!x || !y) return x | y ;
if (a[x].v > a[y].v || (a[x].v == a[y].v && a[x].id > a[y].id)) swap(x, y) ;
rs[x] = merge(rs[x], y) ;
if (dis[ls[x]] < dis[rs[x]]) swap(ls[x], rs[x]) ;
dis[x] = dis[rs[x]] + 1 ;
return x ;
}
再来看插入点和删除点。
插入
直接把新的点当成一个堆,进行合并即可
删除根
直接将根的左右儿子合并即可
删除其他节点
先将左右儿子合并,然后自底向上更新 \(\mathrm{dist}\)、不满足左偏性质时交换左右儿子,当 \(\mathrm{dist}\) 无需更新时结束递归
void pushup(int x) {
if (!x) return;
if (t[x].d != t[rs(x)].d + 1) {
t[x].d = t[rs(x)].d + 1;
pushup(t[x].fa);
}
}
void erase(int x) {
int y = merge(t[x].ch[0], t[x].ch[1]);
t[y].fa = t[x].fa;
if (t[t[x].fa].ch[0] == x)
t[t[x].fa].ch[0] = y;
else if (t[t[x].fa].ch[1] == x)
t[t[x].fa].ch[1] = y;
pushup(t[y].fa);
}
AC自动机
今天才写了板子
算法解释:trie树上看毛片(KMP)
和KMP一样,这两个算法都有失配指针,但是具体的含义不同
具体求解失配指针
Trie树的失配指针是指向:沿着其父节点 的 失配指针,一直向上,直到找到拥有当前这个字母的子节点 的节点 的那个子节点。
听起来有点曹丹啊,并非如此。画图理解一下就行
代码很简单
inline void work() {
queue<int> Q;
for (int i = 0; i < 26; i++) {
if (!tr[0][i]) continue;
fail[tr[0][i]] = 0;
Q.push(tr[0][i]);
}
while (!Q.empty()) {
int x = Q.front();
Q.pop();
for (int i = 0; i < 26; i++) {
if(tr[x][i]) {
fail[tr[x][i]] = tr[fail[x]][i];
in[tr[fail[x]][i]]++;
Q.push(tr[x][i]);
}
else tr[x][i] = tr[fail[x]][i];
}
}
}
之后直接和trie一样建树即可,查询的时候直接暴力跳fail指针即可
拓扑排序优化
由于查询的时候暴力跳fail指针复杂度有点过高,考虑优化一下,发现每个点的fail指针只有一个,那我们直接做一个拓扑排序,之后一次性算出每个点的贡献即可
inline void query(string x) {
int len = x.size(), p = 0;
for (int i = 0; i < len; i++) {
int g = x[i] - 'a';
p = tr[p][g];
tot[p]++;
}
}
inline void toposort() {
queue<int> Q;
for (int i = 1; i <= cnt; i++) if (!in[i]) Q.push(i);
while(!Q.empty()){
int x = Q.front();
Q.pop();
ans[d[x]].num = tot[x];
int v = fail[x];
tot[v] += tot[x];
if (!--in[v]) Q.push(v);
}
}
int m
兔队线段树
用来在二维平面内维护一个关于某个东西单调递增的序列最大长度
考虑到能看到的楼房构成一个斜率单调递增的序列
考虑线段树维护每个区间楼房的斜率最大值,以及以每个左端点为开头的单调序列长度
那么来考虑如何维护
建树:貌似不需要
懒标记:好像也不需要
pushup:这个就需要了考虑如何合并两个区间,设合并后的区间为 \(s\) ,合并前的左右区间为 \(l, r\)
,那么合并后的区间斜率最大值就是 \(l, r\) 的最大值,接下来考虑单调序列长度的合并,这个东西合并后一定会包含 \(l\) 的单调长度,那么 \(r\) 呢?
我们可以先查找 \(r\) 的左区间的最大值,如果这个最大值比 \(l\) 的最大值小,那么 \(r\) 的左区间的所有答案一定看不到,所以我们就可以递归查找 \(r\) 的右区间
如果 \(r\) 的左区间的最大值比 \(l\) 的最大值大,那么原来被 \(r\) 的左区间挡住的现在一样会被挡住,我们就可以加上 \(r\) 的答案,所以我们可以递归查找 \(r\) 的左区间
但是这里有一个坑点,有区间的答案不一定是ans[右区间的右区间],因为右区间的答案可能被左区间挡住,所以有区间的答案一定是ans[右区间]-ans[右区间的左区间]
四边形不等式优化dp
感觉这东西很牛逼
先给一个例子
在石子合并中的转移式
\(dp_{l, r} = \max dp_{l, k} + dp_{k + 1, r} + w(l, r)\)
四边形不等式就是用来优化这种东西的。
这个转移式正常做是 \(O(n^3)\) 的,但是只要 \(w\) 函数满足一些性质,即可优化到 \(O(n^2)\)
具体来说是这样的性质
第一是满足区间包含单调性
说人话就是对于 \(l \le l' \le r' \le r\) 都有 \(w(l', r') \le w(l, r)\)
第二个是满足四边形不等式
其实就是交叉的区间和小于包含的区间和
即 \(l \le l' \le r' \le r\),有 \(w(l, r') + w(l', r) \le w(l, r) + w(l', r')\)
对于满足这些条件的 \(w\) 又有以下这些性质
1.\(dp_{l,r}\) 也满足四边形不等式。
2.\(dp_{l, r}\) 的最优决策点满足单调性 \(\displaystyle pos_{l, r - 1} \le pos_{l, r} \le pos_{l + 1, r}\)
啥是最优决策点,就是 \(dp_{l, r} = \max dp_{l, k} + dp_{k + 1, r} + w(l, r)\) 中可以达到 \(\max\) 的 \(k\)。
换句话说就是最优决策点在每一行每一列是单调不降的,这点十分重要,对于一些dp题中,我们需要去掉一个 \(n\) 的复杂度时,我们可以通过打表看看最优决策点是否具有单调性,如果有,那大概率可以直接优化。
最后优化后的代码:
for (int i = 1; i <= n; ++i)
m[i][i] = i; // 初始化边界决策点
for (int d = 2; d <= n; ++d)
for (int l = 1, r = d; r <= n; ++l,++ r)
{
dp[l][r] = INF;
for (int k = m[l][r - 1]; k <= m[l + 1][r]; ++k) // 利用结论,缩小了枚举范围
if (dp[l][k] + dp[k + 1][r] + w(l, r) < dp[l][r])
{
dp[l][r] = dp[l][k] + dp[k + 1][r] + w(l, r); // 更新dp数组
m[l][r] = k; // 更新决策点
}
}
然后两道例题
P4767 邮局
CF321E Ciel and Gondolas