數據結構|序列問題與樹上問題小結


數據結構小結

好累啊這幾天沉迷數據結構
高數被我鴿幾天了,單詞又背了遍abandon...
總結一下這幾天沉迷的成果,這些東西雖然好玩,但是留給我的時間不多了,2020都過了好多天了
感覺要是不看愛情公寓,可以再多刷個兩道題。。

珂朵莉樹

之前寫過了,https://www.cnblogs.com/fisherss/p/12182869.html

線段樹常用模板

單點更新,區間更新,維護最值

https://www.cnblogs.com/fisherss/p/10920642.html

P3373 線段樹維護區間乘、區間加

題解:https://www.luogu.com.cn/problemnew/solution/P3373
多個標記分清楚,標記的優先級順序

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e5+10;
ll mod;
ll a[maxn];

struct node{
	ll v,mul,add;
}tree[maxn*4+10];

void pushup(int o){
	tree[o].v = (tree[o<<1].v + tree[o<<1|1].v)%mod;
}

void pushdown(int o,int l,int r){
	//這里要干什么
	int mid = (l+r)>>1;
	//更新子節點的sum: 子節點sum = v*父節點mul + 父節點標記的add值 * 子節點區間長度 
	tree[o<<1].v = (tree[o<<1].v * tree[o].mul + tree[o].add * (mid-l+1))%mod;
	tree[o<<1|1].v = (tree[o<<1|1].v * tree[o].mul + tree[o].add * (r-(mid+1)+1))%mod;
	//更新子節點的mul
	tree[o<<1].mul = (tree[o<<1].mul * tree[o].mul)%mod;
	tree[o<<1|1].mul = (tree[o<<1|1].mul * tree[o].mul)%mod;
	//更新子節點的add
	tree[o<<1].add = (tree[o<<1].add * tree[o].mul + tree[o].add)%mod; //先成后加
	tree[o<<1|1].add = (tree[o<<1|1].add * tree[o].mul + tree[o].add)%mod;
	//清除父節點的標記 
	tree[o].mul = 1;
	tree[o].add = 0; 
}

void build(int o,int l,int r){
	//初始化所有結點的lazy標記 
	tree[o].mul = 1;
	tree[o].add = 0;
	if(l == r){ //葉節點 
		tree[o].v = a[l]%mod;
		return;
	}
	int mid = (l+r)>>1;
	build(o<<1,l,mid);
	build(o<<1|1,mid+1,r);
	pushup(o);
}

//區間乘 
void update1(int o,int l,int r,int ql,int qr,ll k){
	if(ql<=l && r<=qr){
		tree[o].v = (tree[o].v * k)%mod;
		tree[o].mul = (tree[o].mul * k)%mod;
		tree[o].add = (tree[o].add * k)%mod;
		return; 
	}
	pushdown(o,l,r);
	int mid = (l+r)>>1;
	if(ql<=mid) update1(o<<1,l,mid,ql,qr,k);
	if(qr>mid) update1(o<<1|1,mid+1,r,ql,qr,k);
	pushup(o);
}

//區間加 
void update2(int o,int l,int r,int ql,int qr,ll k){
	if(ql<=l && r<=qr){
		tree[o].v = (tree[o].v + (r-l+1)*k)%mod;
		tree[o].add = (tree[o].add + k)%mod;
		return;
	}
	pushdown(o,l,r);
	int mid = (l+r)>>1;
	if(ql<=mid) update2(o<<1,l,mid,ql,qr,k);
	if(qr>mid) update2(o<<1|1,mid+1,r,ql,qr,k);
	pushup(o);
}

//區間查詢 
ll querysum(int o,int l,int r,int ql,int qr){
	if(ql<=l && r<=qr){
		return tree[o].v%mod; 
	}
	pushdown(o,l,r);
	int mid = (l+r)>>1;
	ll ans = 0;
	if(ql <= mid) ans = (ans + querysum(o<<1,l,mid,ql,qr))%mod;
	if(qr >= mid+1) ans = (ans + querysum(o<<1|1,mid+1,r,ql,qr))%mod;
	return ans%mod;
}

int main(){
    int n, m;
    scanf("%d%d%d", &n, &m, &mod);
    for(int i=1; i<=n; i++){
        scanf("%lld", &a[i]);
    }
    build(1, 1, n);
    while(m--){
        int opt;
        scanf("%d", &opt);
        int x, y;
        long long k;
        if(opt==1){ //區間乘 
            scanf("%d%d%lld", &x, &y, &k);
            update1(1, 1, n, x, y, k);
        }else if(opt==2){ //區間加 
            scanf("%d%d%lld", &x, &y, &k);
            update2(1, 1, n, x, y, k);
        }else{ //查詢區間和 
            scanf("%d%d", &x, &y);
            printf("%lld\n", querysum(1, 1, n, x, y));
        }
    }
    return 0;
}

P4145 線段樹區間開根號

考慮到開根號是個很快的操作,一個區間開着開着就變成了0或者1
維護下線段樹的某個區間是不是全0或者全1,是的話不管,不是的話暴力開根號即可。


//區間修改 轉變成了 →單點暴力開根號↓
//每進入一個最小區間(只有一個值)開根號,更新標記,向上up更新
void change(int o,int l,int r,int ql,int qr) {
	if(setv[o]) return;
	if(l==r) { //每進入一個最小區間(l==r時區間SSEd@只有一個值)
		sum[o]=(ll)sqrt(sumv[o]);//開根號
		if(sumv[o]==1||sumv[o]==0) setv[o]=1; //新標記
		return;
	}
	int mid = (l+r)>>1;
	if(ql<=mid) change(lson,mid,ql,qr);
	if(qr>mid) change(rson,mid+1,r,ql,qr);
	pushup(o); //向上up更新
}

P4513 線段樹維護最大子段和

這道題學習參考點:主要是"合並"
對於每個區間,維護一個左邊的最大前綴,右邊的最大后綴,以及區間內部的總答案
每次合並的時候,即答案選取左子區間的max,右子區間的max,或者左子區間的最大后綴,右子區間的最大前綴即可
題解1:https://www.luogu.com.cn/blog/41302/solution-p4513
題解2:https://www.luogu.com.cn/blog/user52559/solution-p4513
借個圖

P2572 珂朵莉樹|線段樹維護復雜信息

這道題還可以用珂朵莉樹做,暴力優雅。
線段樹題解1:https://www.luogu.com.cn/blog/QVQ/solution-p2572
線段樹的合並思想類似小白逛公園,分左、右、跨越區間三種清空;
優先級的問題類似於區間乘那題乘法優先,這里就是賦值優先
借個圖

P1712 雙指針 + 線段樹(待補)

題目:https://www.luogu.com.cn/problem/P1712
題解:https://www.luogu.com.cn/blog/user5680/solution-p1712

然后接觸到,線段樹動態開點

思想學習:https://blog.csdn.net/lvmaooi/article/details/79729437
理解了,動態開點就是 動態給結點分配編號,不用先build建樹了
動態開點,每個結點最多開logn個,所以空間復雜度就是O(nlogn)
*又聽說,類似主席樹的寫法,到時候學到再補

P1908 逆序對,線段樹+動態開點做法

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define lson l,mid,tree[root].l
#define rson mid+1,r,tree[root].r
#define ls tree[root].l
#define rs tree[root].r
const int maxn = 5e5+10;
const int inf = 1e9+5;

//為什么要動態開點:因為這道題數字個數大1e9,不動態開點就先要離散化再建線段樹 
//靜態開點的左孩子是root*2,右孩子是root*2+1,現在按編號分配 

struct node{
	int l,r,sum;
}tree[maxn*32];

int cnt = 1;

//合並更新父節點 
void pushup(int root){
	tree[root].sum=tree[tree[root].l].sum+tree[tree[root].r].sum;
} 

void update(int &root,int l,int r,int pos){
	if(!root) root = ++cnt; //動態分配"結點編號"  
	if(l == r){
		tree[root].sum++;
		return;
	}
	int mid = (l + r)>>1;
	if(pos <= mid) update(tree[root].l,l,mid,pos); //要更新的pos點在前半段區間 
	else update(tree[root].r,mid+1,r,pos); //要更新的pos點在后半段區間
	pushup(root);
}

ll query(int root,int l,int r,int ql,int qr){
	ll ans = 0;
	if(ql <= l && r <= qr){ //如果完全包含(l,r)區間 
		return tree[root].sum;
	}
	int mid = (l + r)>>1;
	if(ql <= mid) ans += query(tree[root].l,l,mid,ql,qr); //要查詢的區間包含了左邊一側 查左邊 
	if(qr > mid) ans += query(tree[root].r,mid+1,r,ql,qr); //要查詢的區間包含了右邊一側 查右邊 
	return ans;
} 

int main(){
	int n;
	cin>>n;
	ll ans = 0;
	int root = 1;
	for(int i=1;i<=n;i++){
		int x;
		cin>>x;
		ans += query(1,1,inf,x+1,inf); //查詢當前輸入下已經比x大的數右多少個,即(x+1,inf)范圍內已插入數的個數 
		update(root,1,inf,x); //x位置上個數+1 
	} 
	cout<<ans<<endl;
	return 0;
}

/*
參考鏈接:https://blog.csdn.net/qq_43906000/article/details/102155429
類似題目:https://blog.csdn.net/u012972031/article/details/88751811 
*/

P1908 線段樹 + 離散化做法

#include<bits/stdc++.h>
#define fi first
#define se second
#define INF 0x3f3f3f3f
#define ll long long
#define ld long double
#define mem(ar,num) memset(ar,num,sizeof(ar))
#define me(ar) memset(ar,0,sizeof(ar))
#define lowbit(x) (x&(-x))
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define lcm(a,b) ((a)*(b)/(__gcd((a),(b))))
#define maxn 500010
#define mod 1000000007
using namespace std;

ll n, tree[maxn << 2], arr[maxn], temp[maxn], ans;

void pushup(int root) {
    tree[root] = tree[root << 1] + tree[root << 1 | 1];
}

//初始化各個值都是0 
void build(int root,int l,int r){
	if(l == r){
		tree[root] = 0;
		return; 
	}
	int mid = (l+r)>>1;
	build(root << 1,l,mid);
	build(root <<1 | 1,mid+1,r);
	pushup(root);
}

void update(int root,int l, int r,int pos) {
    if(l == r) {
        tree[root]++;
        return;
    }
    int mid = (l + r) >> 1;
    if(mid >= pos) update(root << 1, l, mid, pos);
    else update(root << 1 | 1,mid + 1, r, pos);
    pushup(root);
}

ll query( int root,int ql, int qr, int l, int r) {
    if(ql <= l && r <= qr) {
        return tree[root];
    }
    int mid = (r + l) >> 1;
    ll ans = 0;
    if(ql <= mid) ans += query(root << 1, ql, qr, l, mid);
    if(qr > mid) ans += query(root << 1 | 1,ql, qr, mid + 1, r);
    return ans;
}

int main() {
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> arr[i], temp[i] = arr[i];
    //離散化開始
    sort(temp + 1, temp + n + 1);
    int num = unique(temp + 1, temp + n + 1) - temp - 1;
    for(int i = 1; i <= n; i++)
        arr[i] = lower_bound(temp + 1, temp + num + 1, arr[i]) - temp; //lower_bound - tempp 就是編號了 
    //離散化結束↑ 
	build(1,1,50001); //這一步可以省略 無需先建樹 
    for(int i = 1; i <= n; i++) {
        update(1, 1, n, arr[i]);
        ans += query(1, arr[i] + 1, n, 1, n);
    }
    cout << ans;
    return 0;
}

/*
參考鏈接:https://blog.csdn.net/endeavor_g/article/details/88654684 
*/

HPU校賽 線段樹動態開點維護前綴和后綴和

敲了一遍竟然過了,有點開心啊!
https://www.cnblogs.com/fisherss/p/12104701.html

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e5+10;
int n;
ll a[maxn];
int k[maxn];


ll min(ll a,ll b){
	if(a < b) return a;
	else return b;
}

struct tree{
	ll sum[maxn];
	
	void pushup(int root){
		sum[root] = min(sum[root<<1],sum[root<<1|1]);
	}
	
	void build(int root,int l,int r){
		if(l == r){
			sum[root] = 0;
			return;
		}
		int mid = (l+r)>>1;
		build(root<<1,l,mid);
		build(root<<1|1,mid+1,r);
		pushup(root);
	}
	
	void update(int root,int l,int r,int pos,int v){
		if(l == r){
			sum[root] = v;
			return;
		}
		int mid = (l + r) >> 1;
		if(pos <= mid) update(root<<1,l,mid,pos,v);
		else update(root<<1|1,mid+1,r,pos,v);
		pushup(root);
	}
	
	ll query(int root,int l,int r,int ql,int qr){
		if(ql == 0 && qr == 0) return 0; //必須處理 查詢長度為0時的邊界 
		if(ql <= l && r <= qr){
			return sum[root];
		}
		int mid = (l + r) >> 1;
		ll ans = 0x3f3f3f3f;
		if(ql <= mid) ans = min(ans,query(root<<1,l,mid,ql,qr));
		if(qr > mid) ans = min(ans,query(root<<1|1,mid+1,r,ql,qr));
		return ans; 
	}
	
}tp,tn;

int main(){
	cin>>n;
	for(int i=1;i<=n;i++) cin>>k[i];
	for(int i=1;i<=n;i++) cin>>a[i];
	ll sum = 0;
	tp.build(1,1,n);
	for(int i=1;i<=n;i++){
		sum += a[i];//前綴
		tp.update(1,1,n,i,sum); 
	}
	sum = 0;
	tn.build(1,1,n);
	for(int i=n;i>=1;i--){
		sum += a[i];//后綴 
		tn.update(1,1,n,i,sum);
	}
	ll ans = 0;
	//求s1 + ... + sn 
	for(int i=1;i<=n;i++){
		ans += sum;
		ans -= tp.query(1,1,n,max(i-k[i]-1,0),max(i-1,0)); //除了i以外的前綴 
		ans -= tn.query(1,1,n,min(i+1,n+1),min(i+k[i]+1,n+1)); //除了i以外的后綴
	} 
	cout<<ans;
	return 0; 
} 

/*
5
1 2 3 4 4
-5 1 2 3 -4


16
*/

掃描線 與 線段樹問題

講解最好的博客1:https://blog.csdn.net/xianpingping/article/details/83032798
HDU1542參考題解2:https://www.cnblogs.com/liwenchi/p/7259171.html

借用一下博客1的代碼,加了部分注釋

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<string>
#include<queue>
#include<algorithm>
#include<map>
#include<iomanip>
#define INF 99999999
using namespace std;

/*
個人感覺:
	相當於線段樹在線開點 插入新點 維護sum邊長 
*/ 
 
const int MAX=200+10;
int mark[MAX<<2];//記錄某個區間的下底邊個數
double sum[MAX<<2];//記錄某個區間的下底邊總長度
double value[MAX];//對x進行離散化,否則x為浮點數且很大無法進行線段樹 
 
//以橫坐標作為線段(區間),對橫坐標線段進行掃描
//掃描的作用是每次更新下底邊總長度和下底邊個數,增加新面積 
struct seg{//線段
	double l,r,h;
	int d;
	seg(){}
	seg(double x1,double x2,double H,int c):l(x1),r(x2),h(H),d(c){}
	bool operator<(const seg &a)const{
		return h<a.h;
	}
}s[MAX];

void pushup(int o,int left,int right){ 
	if(mark[o]) sum[o]=value[right+1]-value[left];//mark[o]!=0表示包含了整個子區間,該子區間整個線段長度可以作為底邊 
	else if(left == right)sum[o]=0;//葉子結點則底邊長度為0(區間內線段長度為0) 
	else sum[o]=sum[o<<1]+sum[o<<1|1];
}
 
void update(int L,int R,int d,int o,int left,int right){
	if(L<=left && right<=R){//該區間是當前掃描線段的一部分,則該區間下底邊總長以及上下底邊個數差更新 
		mark[o]+=d;//更新底邊相差差個數 
		pushup(o,left,right);//更新底邊長 
		return;
	}
	int mid=left+right>>1;
	if(L<=mid)update(L,R,d,o<<1,left,mid);
	if(R>mid)update(L,R,d,o<<1|1,mid+1,right);
	pushup(o,left,right);
}

//二分查找 
int search(double key,double* x,int n){
	int left=0,right=n-1;
	while(left<=right){
		int mid=left+right>>1;
		if(x[mid] == key)return mid;
		if(x[mid]>key)right=mid-1;
		else left=mid+1;
	}
	return -1;
}
 
int main(){
	int n,num=0;
	double x1,x2,y1,y2;
	while(cin>>n,n){
		int k=0;
		for(int i=0;i<n;++i){
			cin>>x1>>y1>>x2>>y2;
			value[k]=x1; //記錄樹結點->但是記錄的離散化前的 
			s[k++]=seg(x1,x2,y1,1); //記錄掃描線 下邊位 
			value[k]=x2;
			s[k++]=seg(x1,x2,y2,-1); //記錄掃描線 上邊位 
		}
		sort(value,value+k);
		sort(s,s+k);
		int m=1;
		for(int i=1;i<k;++i)//去重復端點 
		    if(value[i] != value[i-1])value[m++]=value[i];
        double ans=0;
        //memset(mark,0,sizeof mark);
        //memset(sum,0,sizeof sum);如果下面是i<k-1則要初始化,因為如果對第k-1條線段掃描時會使得mark,sum為0才不用初始化的 
		for(int i=0;i<k;++i){//掃描線段
			int L=search(s[i].l,value,m);
			int R=search(s[i].r,value,m)-1;
			update(L,R,s[i].d,1,0,m-1);//掃描線段時更新底邊長度和底邊相差個數
//			cout<<ans<<" "<<sum[1]*(s[i+1].h-s[i].h)<<endl;
			ans+=sum[1]*(s[i+1].h-s[i].h);//新增加面積
		}
		printf("Test case #%d\nTotal explored area: %.2lf\n\n",++num,ans);
	}
	return 0;
}
/*
這里注意下
掃描線段時r-1:int R=search(s[i].l,value,m)-1;
計算底邊長時r+1:if(mark[n])sum[n]=value[right+1]-value[left];
解釋:假設現在有一個線段左端點是l=0,右端點是r=m-1
則我們去更新的時候,會算到sum[1]=value[mid]-value[left]+value[right]-value[mid+1]
這樣的到的底邊長sum是錯誤的,why?因為少算了mid~mid+1的距離,由於我們這利用了
離散化且區間表示線段,所以mid~mid+1之間是有長度的,比如value[3]=1.2,value[4]=5.6,mid=3
所以這里用r-1,r+1就很好理解了 
*/ 

樹鏈剖分

學習地址1:https://www.bilibili.com/video/av4482146
學習地址2:https://www.bilibili.com/video/av24798851
詳細博客1:https://www.cnblogs.com/chinhhh/p/7965433.html
用法總結1:https://blog.csdn.net/qq_41730604/article/details/101453877
樹剖入門到入土的題目總結:https://www.cnblogs.com/Isaunoya/p/11619823.html

樹鏈剖分求LCA(模板P3379)

我我我終於寫出了自己的LCA!
還是喜歡vector啊

#include<bits/stdc++.h>
using namespace std;

const int maxn = 5e5+100;
vector<int> g[maxn];
/*  父親,    深度,       子節點數, 重兒子,   dfs序,   dfs映射,鏈頭,    鏈尾    */
int fa[maxn],depth[maxn],sz[maxn],son[maxn],id[maxn],rk[maxn],top[maxn],bot[maxn];
int cnt = 0;

void dfs(int x,int deep){
	depth[x] = deep;
	sz[x] = 1;
	for(int li = 0;li<g[x].size();li++){
		int i = g[x][li];
		if(i == fa[x]) continue;
		fa[i] = x;
		dfs(i,deep+1);
		sz[x] += sz[i];
		if(sz[i] > sz[son[x]]) son[x] = i;
	}
}

void dfs2(int x,int tp){
	top[x] = tp;
	id[x] = ++cnt;
	rk[cnt] = x;
	if(son[x]) dfs2(son[x],tp),bot[x] = bot[son[x]];
	else bot[x] = x;
	for(int li=0;li<g[x].size();li++){
		int i = g[x][li];
		if(i != fa[x] && i != son[x])
			dfs2(i,i);
	}
}

int lca(int u,int v){
	while(top[u] != top[v]){
		if(depth[top[u]] < depth[top[v]]) swap(u,v);
		u = fa[top[u]];
	}
	if(depth[u] < depth[v]) return u;
	return v;
}

int main(){
	ios::sync_with_stdio(false);
	int n,m,root;
	cin>>n>>m>>root;
	for(int i=1;i<=n-1;i++){
		int u,v;
		cin>>u>>v;
		g[u].push_back(v);
		g[v].push_back(u);
	}
	dfs(root,1);
	dfs2(root,root);
	while(m--){
		int u,v;
		cin>>u>>v;
		cout<<lca(u,v)<<endl;
	}
	return 0;
}

還有很多...吃不消了

待補


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM