• 周五. 7月 1st, 2022

5G编程聚合网

5G时代下一个聚合的编程学习网

热门标签

7.14复习笔记

admin

11月 28, 2021

一、线段树优化建图

线段树优化建图可以用来优化区间向单点,单点向区间,区间向区间连边的问题,可以将边数从(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
",lst);
	}
	return 0;
} 
/*
0
3 2
0 1 2
1 0 2
1 1 2
*/

五、斜率优化

斜率优化的条件:

1.只在一维转移

2.出现了(i imes j)这一项

3.满足决策单调性

斜率优化的流程:

1.写出dp转移方程

2.找出对应的x((i imes j)项里的j),y(只含j的项),k((i imes 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 imes sum[i] imes sum[j]+sum[j]^2)

此时我对应的(x = sum[j],y = dp[j]+sum[j]^2,k = 2 imes 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
",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; 
}

发表评论

您的电子邮箱地址不会被公开。