7.14复习笔记
一、线段树优化建图
线段树优化建图可以用来优化区间向单点,单点向区间,区间向区间连边的问题,可以将边数从\(qn\)级别降至\(qlogn\)级别
具体的引入两道题完全包含上述所说的问题:
1. CF786B Legacy
先建出一颗出树一颗入树(不同题下不一定两颗树都要建)
void build(int &x,int l,int r,int op){
if (l == r){x = l;return;}
x = ++tot;
int mid = (l+r >> 1);
build(ls[x],l,mid,op),build(rs[x],mid+1,r,op);
if (op == 1) ed.add(x,ls[x],0),ed.add(x,rs[x],0);
if (op == 2) ed.add(ls[x],x,0),ed.add(rs[x],x,0);
}
然后是区间向单点和单点向区间连边,这两个操作可以看成是互逆的
void modify(int x,int l,int r,int nl,int nr,int p,int w,int op){
if (nl <= l&&r <= nr){
if (op == 2) ed.add(p,x,w);
if (op == 3) ed.add(x,p,w);
return;
}
int mid = (l+r >> 1);
if (nl <= mid) modify(ls[x],l,mid,nl,nr,p,w,op);
if (nr > mid) modify(rs[x],mid+1,r,nl,nr,p,w,op);
}
2.[PA2011]Journeys
这道题就是剩下的那个区间向区间连边了
我们可以每一条边都建一个虚点,从区间向虚点连边,从虚点向区间连边,双向边连两次
注意:一定要看好出树和入树的连边方向,从出树连出来连向入树
void modify(int x,int l,int r,int nl,int nr,int op){
if (nl <= l&&r <= nr){
if (op == 2) ed.add(x,tot,1);
if (op == 1) ed.add(tot,x,1);
return;
}
int mid = (l+r >> 1);
if (nl <= mid) modify(ls[x],l,mid,nl,nr,op);
if (nr > mid) modify(rs[x],mid+1,r,nl,nr,op);
}
二、最短路
既然上面写线段树优化建图的时候都把Dij给写了,那我就直接再复习一下最短路吧
三种最短路的板子:
void Dij(int s){
priority_queue<pair<int,int> > q;q.push(make_pair(0,s));
for (int i = 1;i <= tot;i++) dis[i] = inf,vis[i] = 0;
dis[s] = 0;
while (!q.empty()){
int x = q.top().second;q.pop();
if (vis[x]) continue;
vis[x] = 1;
for (int i = ed.head[x];i;i = ed.nxt[i]){
int to = ed.to[i];
if (dis[to] > dis[x]+ed.w[i]){
dis[to] = dis[x]+ed.w[i];
q.push(make_pair(-dis[to],to));
}
}
}
}
void SPFA(){
queue<int> q;q.push(s);
for (int i = 1;i <= n;i++) dis[i] = inf,vis[i] = 0;
vis[1] = 1,dis[1] = 0;
while (!q.empty()){
int x = q.front();q.pop();
vis[x] = 0;
for (int i = head[x];i;i = ed[i].nxt){
int to = ed[i].to;
if (dis[to] > dis[x]+ed[i].w){
dis[to] = dis[x]+ed[i].w;
if (!vis[to]) q.push(to);
}
}
}
}
void Floyd(){
for (int k = 1;k <= n;k++){
for (int i = 1;i <= n;i++){
for (int j = 1;j <= n;j++){
dis[i][j] = min(dis[i][k]+dis[k][j],dis[i][j]);
}
}
}
}
一些问题:
1.从1号点出发,到每个点的最短路有多少条?
跟dp一样,在转移的时候统计方案就好了
if (dis[to] == dis[x]+ed.w[i]) dp[to] += dp[x];
else if (dis[to] > dis[x]+ed.w[i]){
dis[to] = dis[x]+ed.w[i];
dp[to] = dp[x];
q.push(make_pair(-dis[to],to));
}
2.给定一条边e,求有多少条经过边e的从1到n的最短路?
经过这条边的方式有两种:从u走到v和从v走到u
解决这个问题首先我们需要知道边是否在最短路上,看dis的差值等不等于边权就好了
如果这条边在最短路上,结合问题1,先走的点有多少条最短路,这条边就有多少条最短路
3.给定一条边e,请问这条边是否一定在从1到n的最短路上?
结合问题2,看最短路条数是否为1
特殊最短路
BFS(边权全一样),01 BFS(边权只有0和1),多源Dijkstra
1.01BFS
考虑正常的BFS,我们可以想象到,其实就是在维护一个距离单调的队列,01BFS也是一样,只是队列中只有x和x+1
因为我们每次边权只会加上0或者1,加上0不变,他还是最小的直接塞到头,加上1变大塞到末尾
2.多源Dijkstra
可以建一个虚点,虚点向每一个起点连一条边权为0的边,然后跑最短路
但是我们发现这样没什么用,我们可以直接在入队的时候把所有源点加进队列里然后跑最短路
三、exgcd
我都有点忘了怎么求的了,虽然我一直都不会推导。
扩展欧几里得算法(其实过程和gcd很类似),可以求解类似\(ax+by = c\)的二元方程的一组解,其中\(gcd(a,b)|c\)
我们在具体做题的时候可以分成以下几个流程:
1. 写出同余方程,找到\(a,b,c\)(一般b是模数)
2. 然后\(a\)和\(b\)同时除以\(gcd(a,b)\),套模板求exgcd
3. 最后乘上\(c\)/\(gcd(a,b)\),并对\(b\)取模
void exgcd(int a,int b,int &x,int &y){
if (!b){x = 1,y = 0;return;}
exgcd(b,a%b,y,x);
y -= (a/b)*x;
}
四、可持久化数据结构
这里主要是讲一下可持久化线段树,也叫主席树
主席树对比与线段树的一个优点是他可以查询历时版本,而且还运用了前缀和的思想,注意这里提醒我们需要前缀和可以参考主席树
看一道例题,这是我一次模拟题的部分分出成的一道简化版:
Rmq Problem / mex
找区间内最小的没有出现过的自然数
每一次新加入一个数,我们就可以值域线段树维护每个数最晚的位置,然后区间取min
每次查询对于一个区间的右端点,找到在这个历史版本下,最大的最晚出现位置<l的那个数
其实转化问题比较重要吧,对于一次查询我们只需要他以及他之前的数的信息,也可以看成一个前缀和?
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#define B cout<<"Breakpoint"<<endl;
#define O(x) cout<<#x<<" "<<x<<endl;
#define o(x) cout<<#x<<" "<<x<<" ";
using namespace std;
int read(){
int x = 1,a = 0;char ch = getchar();
while (ch < '0'||ch > '9'){if (ch == '-') x = -1;ch = getchar();}
while (ch >= '0'&&ch <= '9'){a = a*10+ch-'0';ch = getchar();}
return x*a;
}
const int maxn = 1e7+10,inf = 1e9+7;
int T,n,q;
int tot;
struct node{
int l,r,val;
}tree[maxn];
int b[maxn];
int root[maxn];
void pushup(int x){
tree[x].val = min(tree[tree[x].l].val,tree[tree[x].r].val);
}
void modify(int &x,int lst,int l,int r,int p,int k){
if (!x) x = ++tot;
if (l == r) {tree[x].val = k;return;}
int mid = (l+r >> 1);
if (p <= mid) tree[x].r = tree[lst].r,modify(tree[x].l,tree[lst].l,l,mid,p,k);
else tree[x].l = tree[lst].l,modify(tree[x].r,tree[lst].r,mid+1,r,p,k);
pushup(x);
}
int query(int x,int l,int r,int k){
if (l == r) return l;
int mid = (l+r >> 1);
if (tree[tree[x].l].val < k) return query(tree[x].l,l,mid,k);
else return query(tree[x].r,mid+1,r,k);
}
int a[maxn];
int main(){
// freopen("mexe2-1.in","r",stdin);
// freopen("out.out","w",stdout);
T = read(),n = read(),q = read();
int lst = 0;
for (int i = 1;i <= n;i++) a[i] = read();
for (int i = 1;i <= n;i++) modify(root[i],root[i-1],0,n,a[i],i);
// debug(1,1,n);
for (int i = 1;i <= q;i++){
int op = read(),x = read(),y = read();
x = (x+T*lst) % n+1,y = (y+T*lst) % n+op;
// cout<<x<<" "<<y<<endl;
if (x > y) swap(x,y);
lst = query(root[y],0,n,x);
printf("%d\n",lst);
}
return 0;
}
/*
0
3 2
0 1 2
1 0 2
1 1 2
*/
五、斜率优化
斜率优化的条件:
1.只在一维转移
2.出现了\(i\times j\)这一项
3.满足决策单调性
斜率优化的流程:
1.写出dp转移方程
2.找出对应的x(\(i\times j\)项里的j),y(只含j的项),k(\(i\times j\)项里的i)
是不是看起来还挺简单的?找一道例题:
「SDOI2016」征途
通过一系列的推导,我们可以知道求方差最小,也就是求平方和最小
定义状态:\(dp[i][j]\)表示在前j个物品里分成i组的最小方差
状态转移:\(dp[i][j] = min(dp[i-1][k]+w(k,j),dp[i][j])\)
枚举上一组的最后一个物品,其中\(w(i,j) = (sum[j]-sum[i])^2\)
发现第一维没有转移可以暂时去掉他,然后我们把dp方程展开:
\(dp[i] = dp[j]+sum[i]^2+2\times sum[i]\times sum[j]+sum[j]^2\)
此时我对应的\(x = sum[j],y = dp[j]+sum[j]^2,k = 2\times sum[i]\),就可以斜率优化了
double X(int x){
return (double)sum[x];
}
double Y(int id,int x){
return (double)(dp[id][x]+sum[x]*sum[x]);
}
double slope(int id,int a,int b){
return (Y(id,b)-Y(id,a))/(X(b)-X(a));
}
int main(){
n = read(),m = read();
for (int i = 1;i <= n;i++) a[i] = read();
for (int i = 1;i <= n;i++) sum[i] = sum[i-1]+a[i];
for (int i = 1;i <= n;i++) dp[1][i] = sum[i]*sum[i];
for (int j = 2;j <= m;j++){
int head = 1,tail = 1;
for (int i = 1;i <= n;i++){
while (head < tail&&slope(j-1,q[head],q[head+1]) <= 2*sum[i]) head++;
dp[j][i] = dp[j-1][q[head]]+(sum[i]-sum[q[head]])*(sum[i]-sum[q[head]]);
while (head < tail&&slope(j-1,q[tail],i) <= slope(j-1,q[tail-1],q[tail])) tail--;
q[++tail] = i;
}
}
printf("%d\n",m*dp[m][n]-sum[n]*sum[n]);
return 0;
}
六、决策单调性优化
今天上课学到的:一般大于1次的方程转移都是满足决策单调性的
决策单调性优化分为两种
1.每个阶段的被决策点不会成为决策点
这类问题的dp状态通常是二维的,就比如上面的问题
假设被决策点的范围在[l,r]之间,决策点的范围在[nl,nr]之间
对于每一个选取的区间的mid,我们需要找到他的决策点然后继续向下分治
void solve(int x,int l,int r,int nl,int nr){
if (l > r) return;
int mid = (l+r >> 1),pos;
for (int i = nl;i <= min(mid,nr);i++){
int w = dp[x-1][i]+(sum[mid]-sum[i])*(sum[mid]-sum[i]);
if (w < dp[x][mid]) dp[x][mid] = w,pos = i;
}
solve(x,l,mid-1,nl,pos),solve(x,mid+1,r,pos,nr);
}
2.每个阶段的被决策点可能成为决策点
很简单,我们分治套分治就好了,因为cdq分治的时候我们就是在考虑[l,mid]对[mid+1,r]的贡献,思路很相符
因为我自己对分治的理解就不是特别深入,所以讲的就比较粗略
void solve(int l,int r,int nl,int nr){
if (l > r||nl > nr) return;
int mid = (l+r >> 1),pos,lst = inf;
for (int i = nl;i <= min(mid,nr);i++){
int w = calc(i,mid);
if (w < lst) lst = w,pos = i;
}
dp[mid] = min(dp[mid],lst);
solve(l,mid-1,nl,pos),solve(mid+1,r,pos,nr);
}
void cdq(int l,int r){
if (l == r) return;
int mid = (l+r >> 1);
cdq(l,mid),solve(mid+1,r,l,mid);
cdq(mid+1,r);
}
七、单调队列优化dp
本章的最后一个小结了!其实我本来都删掉了,但是良心不安啊,还是补上来了
单调队列解决问题的标志: 规定长度内的最值
单调队列优化的流程:
1.一样先写出状态转移方程
2.单调队列优化(太简陋了,但我也想不出来说啥好)
int head = 1,tail = 1;
for (int i = 1;i <= n;i++){
while (q[head]+k < i) head++;
dp[i] = dp[q[head]]+a[i];
while (head < tail&&dp[i] <= dp[q[tail]]) tail--;
q[++tail] = i;
}