数据结构

STL

  1. 向量/栈/队列/优先队列/集合/映射/可重集。

  2. 对顶堆解决中位数相关问题。

    详解

    使用大顶堆维护较小的一半元素,小顶堆维护另一半,时刻保持二者元素个数之差不超过 \(1\)

    P1168 中位数

    Code
    #include<iostream>
    #include<queue>
    #include<vector>
    #include<functional>
    #define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
    using namespace std;
    priority_queue<int,vector<int>,less<int>>max_heap;
    priority_queue<int,vector<int>,greater<int>>min_heap;
    int n,tmp;
    int main()
    {
    	IOS;
    	cin>>n;
    	for(int i=1;i<=n;i++)
    	{
    		cin>>tmp;
    		if(!max_heap.empty()&&tmp>max_heap.top())
    			min_heap.push(tmp);
    		else
    			max_heap.push(tmp);
    		if(max_heap.size()>min_heap.size()+1)
    			min_heap.push(max_heap.top()),max_heap.pop();
    		if(min_heap.size()>max_heap.size()+1)
    			max_heap.push(min_heap.top()),min_heap.pop();
    		if(i&1) cout<<(max_heap.size()>min_heap.size()?max_heap.top():min_heap.top())<<'\n';
    	}
    	return 0;
    }
    
  3. 会启发式合并。

    详解

    在涉及数组/集合/映射的合并中,尤其是在树上自底向上合并时常见,总是将小的向大的合并,可以实现均摊 \(O(n \log n)\) 的时间复杂度。

    需要注意的是,由于合并过程可能会发生 swap 操作,因此最终留在节点 \(u\) 上的容器,内部所保存的值并不一定反映该节点的情况,故需要 merge 完成后及时求解当前节点 \(u\) 的答案。

    CF600E Lomsat gelral

    Code
    #include<iostream>
    #include<map>
    #define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
    using namespace std;
    const int N=1e5+5;
    int n,head[N],nxt[N<<1],to[N<<1],cnt=0,tot[N];
    long long ans[N];
    map<int,int>col[N];
    void Add(int u,int v)
    {
    	to[++cnt]=v;
    	nxt[cnt]=head[u];
    	head[u]=cnt;
    }
    void Dfs(int u,int fa)
    {
    	for(int i=head[u];i;i=nxt[i])
    	{
    		int v=to[i];
    		if(v==fa) continue;
    		Dfs(v,u);
    		if(col[u].size()<col[v].size())
    		{
    			tot[u]=tot[v];//v上的答案已经保存过了,故不需要交换
    			ans[u]=ans[v];
    			swap(col[u],col[v]);
    		}
    		for(auto tmp:col[v])//把v合并到u上
    		{
    			int c=tmp.first,num=tmp.second;
    			col[u][c]+=num;
    			if(col[u][c]>tot[u]) tot[u]=col[u][c],ans[u]=c;
    			else if(col[u][c]==tot[u]) ans[u]+=c;
    		}
    	}
    }
    int main()
    {
    	IOS;
    	cin>>n;
    	for(int i=1;i<=n;i++)
    	{
    		int tmp;
    		cin>>tmp;
    		tot[i]=1,col[i][tmp]=1,ans[i]=tmp;
    	}
    	for(int i=1;i<n;i++)
    	{
    		int u,v;
    		cin>>u>>v;
    		Add(u,v),Add(v,u);
    	}
    	Dfs(1,0);
    	for(int i=1;i<=n;i++)
    		cout<<ans[i]<<' ';
    	return 0;
    }
    

二分

  1. 将最优解转化为存在解,需满足单调性。

双指针

  1. 维护左右两处指针,需满足右指针向后移动时左指针同样单调移动。

单调栈

  1. 维护左侧/右侧最近邻满足某种偏序的所在位置。

    详解

    通过维护单调递增或递减的栈,求解如左侧/右侧第一个大于/小于该位置上的元素。

    P1901 发射站

    Code
    #include<iostream>
    #include<stack>
    #define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
    using namespace std;
    const int N=1e6+5;
    stack<int>stk;
    int n,h[N],v[N],ans[N],maxx;
    int main()
    {
    	IOS;
    	cin>>n;
    	for(int i=1;i<=n;i++)
    		cin>>h[i]>>v[i];
    	for(int i=1;i<=n;i++)
    	{
    		while(!stk.empty()&&h[i]>h[stk.top()])
    			ans[i]+=v[stk.top()],maxx=max(maxx,ans[i]),stk.pop();
    		if(!stk.empty())
    			ans[stk.top()]+=v[i],maxx=max(maxx,ans[stk.top()]);
    		stk.push(i);
    	}
    	cout<<maxx<<'\n';
    	return 0;
    }
    
  2. 常用于在钦定某处取得最值时,寻找最大合法的左右范围。

    详解

    即求以某元素为最大/最小值时,能扩展的最大区间。

    U622779 序列

    Code
    #include<iostream>
    #include<stack>
    #define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
    using namespace std;
    const int N=1e5+5;
    int n,a[N],f[N],g[N];
    long long ans=0;
    stack<int>stk;
    int main()
    {
    	IOS;
    	cin>>n;
    	for(int i=1;i<=n;i++)
    		cin>>a[i];
    	for(int i=1;i<=n;i++)
    	{
    		while(!stk.empty()&&a[i]<a[stk.top()])
    			stk.pop();
    		if(!stk.empty())
    			f[i]=stk.top();
    		stk.push(i);
    	}
    	while(!stk.empty())
    		stk.pop();
    	for(int i=n;i>=1;i--)
    	{
    		while(!stk.empty()&&a[i]<a[stk.top()])
    			stk.pop();
    		if(!stk.empty()) 
    			g[i]=stk.top();
    		else
    			g[i]=n+1;
    		stk.push(i);
    	}
    	for(int i=1;i<=n;i++)
    		ans+=1ll*a[i]*(i-f[i])*(g[i]-i);
    	for(int i=1;i<=n;i++)
    		cout<<f[i]<<' '<<g[i]<<'\n';
    	cout<<ans<<'\n';
    	return 0;
    }
    

单调队列

  1. 维护连续区间内的最值。

    详解

    维护一个双端队列,使其内部元素保持单增/单减。

    需要注意的是,在滑动窗口这类给定区间长度的题目中,不能简单地用队列大小是否大于区间长度来判断队首元素是否过时。

    P1886 滑动窗口 /【模板】单调队列

    Code
    #include<iostream>
    #include<queue>
    #define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
    using namespace std;
    const int N=1e6+5;
    int n,k,a[N];
    deque<int>q;
    int main()
    {
    	IOS;
    	cin>>n>>k;
    	for(int i=1;i<=n;i++)
    		cin>>a[i];
    	for(int i=1;i<=n;i++)
    	{
    		while(!q.empty()&&a[i]<a[q.back()])
    			q.pop_back();
    		q.push_back(i);
    		if(q.front()<i-k+1)//弹出过时元素
    			q.pop_front();
    		if(i>=k)
    			cout<<a[q.front()]<<' ';
    	}
    	cout<<'\n';
    	while(!q.empty())
    		q.pop_back();
    	for(int i=1;i<=n;i++)
    	{
    		while(!q.empty()&&a[i]>a[q.back()])
    			q.pop_back();
    		q.push_back(i);
    		if(q.front()<i-k+1)
    			q.pop_front();
    		if(i>=k)
    			cout<<a[q.front()]<<' ';
    	}
    	return 0;
    }
    
  2. 要求给定区间长或区间右侧向右时左侧单调向右。

  3. 会单调队列优化DP

    详解

    使用单调队列快速查询可以转移至当前状态的最优解,并及时清理过时元素

    P1725 琪露诺

    Code
    #include<iostream>
    #include<queue>
    #include<cstring>
    #define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
    using namespace std;
    const int N=2e5+5;
    int n,L,R,a[N],dp[N],maxx=-0x3f3f3f3f;
    deque<int>q;
    int main()
    {
    	IOS;
    	memset(dp,0xaf,sizeof(dp));
    	//有些位置不存在到达的可能,故需初始化
    	cin>>n>>L>>R;
    	for(int i=0;i<=n;i++)
    		cin>>a[i];
    	dp[0]=0;
    	for(int i=L;i<=n;i++)
    	{
    		while(!q.empty()&&dp[i-L]>dp[q.back()])
    			q.pop_back();
    		q.push_back(i-L);
    		while(!q.empty()&&q.front()<i-R)
    			q.pop_front();
    		if(!q.empty())
    			dp[i]=dp[q.front()]+a[i];
    		if(i+R>n) maxx=max(maxx,dp[i]);
    	}
    	cout<<maxx<<'\n';
    	return 0;
    }
    

前缀和

  1. 静态维护连续区间和/异或和等具有逆运算的区间答案。

    详解

    前缀和可以实现 \(O(1)\) 查询静态区间和

    \(pre_i = pre_{i-1} + a_i\)\(\Sigma_{i=l}^r a_i = pre_r - pre_{l-1}\)

    \(xor_i = xor_{i-1} \oplus a_i\)\(\bigoplus_{i=l}^{r} a_i = xor_r \oplus pre_{l-1}\)

  2. 高维下利用容斥完成,例如二维前缀和。

    详解

    \(pre_{i,j} = pre_{i-1,j} + pre_{i,j-1} - pre_{i-1,j-1} + a_{i,j}\)

    \((a,b)\)\((x,y)\) 的矩阵和:
    \(sum = pre_{x,y} - pre_{a-1,y} - pre{x,b-1} + pre_{a-1,b-1}\)

    P1387 最大正方形

    Code
    #include<iostream>
    #define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
    using namespace std;
    const int N=105;
    int n,m,a[N][N],pre[N][N],ans;
    int Calc(int i1,int j1,int i2,int j2)
    {
    	return pre[i2][j2]-pre[i1-1][j2]-pre[i2][j1-1]+pre[i1-1][j1-1];
    }
    bool Check(int x)
    {
    	for(int i=1;i<=n-x+1;i++)
    		for(int j=1;j<=m-x+1;j++)
    			if(Calc(i,j,i+x-1,j+x-1)==x*x)
    				return true;
    	return false;
    }
    int main()
    {
    	IOS;
    	cin>>n>>m;
    	for(int i=1;i<=n;i++)
    		for(int j=1;j<=m;j++)
    			cin>>a[i][j],pre[i][j]=pre[i-1][j]+pre[i][j-1]-pre[i-1][j-1]+a[i][j];
    	int L=1,R=n;
    	while(L<=R)
    	{
    		int Mid=(L+R)>>1;
    		if(Check(Mid))
    			ans=Mid,L=Mid+1;
    		else
    			R=Mid-1;
    	}
    	cout<<ans<<'\n';
    	return 0;
    }
    
  3. 了解差分、原数组、前缀和之间的相互关系并可推导。

    详解

    $pre_i = pre_{i-1} + a_i,diff_i = a_i - a_{i-1} $

    \(a_i = \Sigma_{j=1}^i diff_j\)\(pre_i = \Sigma_{j=1}^i \Sigma_{k=1}^j diff_k = \Sigma_{j=1}^i (i-j+1) diff_j\)

差分

  1. 区间修改转为头尾差分。

    详解

    差分可以实现 \(O(1)\) 修改,查询前 \(O(n)\) 计算

    对区间 \([L,R]\) 每个元素加上 \(val\)

    \(diff_L += val,diff_{R+1} -= val\)

  2. 树上路径修改转为节点差分。

    点差分

    \(u,v\) 路径上每个点权加上 \(val\)

    \(diff_u +=val,diff_v += val,diff_{lca} -=val,diff_{father_{lca}} -= val\)

    P3128 [USACO15DEC] Max Flow P

    Code
    #include<iostream>
    #define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
    using namespace std;
    const int N=5e4+5;
    const int LogN=20;
    int n,k,head[N],nxt[N<<1],to[N<<1],cnt=0,dep[N],father[N][LogN],diff[N],num[N],maxx;
    void Add(int u,int v)
    {
    	to[++cnt]=v;
    	nxt[cnt]=head[u];
    	head[u]=cnt;
    }
    void Dfs_pre(int u,int fa)
    {
    	dep[u]=dep[fa]+1,father[u][0]=fa;
    	for(int i=1;i<LogN;i++)
    		father[u][i]=father[father[u][i-1]][i-1];
    	for(int i=head[u];i;i=nxt[i])
    	{
    		int v=to[i];
    		if(v==fa) continue;
    		Dfs_pre(v,u);
    	}
    }
    int LCA(int u,int v)
    {
    	if(dep[u]<dep[v]) 
    		swap(u,v);
    	for(int i=LogN-1;i>=0;i--)
    		if(dep[father[u][i]]>=dep[v])
    			u=father[u][i];
    	if(u==v) return u;
    	for(int i=LogN-1;i>=0;i--)
    		if(father[u][i]!=father[v][i])
    			u=father[u][i],v=father[v][i];
    	return father[u][0];
    }
    void Dfs_calc(int u,int fa)
    {
    	num[u]=diff[u];
    	for(int i=head[u];i;i=nxt[i])
    	{
    		int v=to[i];
    		if(v==fa) continue;
    		Dfs_calc(v,u);
    		num[u]+=num[v];
    	}
    }
    int main()
    {
    	IOS;
    	cin>>n>>k;
    	for(int i=1;i<n;i++)
    	{
    		int u,v;
    		cin>>u>>v;
    		Add(u,v),Add(v,u);
    	}
    	Dfs_pre(1,0);
    	while(k--)
    	{
    		int s,t,tmp;
    		cin>>s>>t;
    		tmp=LCA(s,t);
    		diff[s]++,diff[t]++;
    		diff[tmp]--,diff[father[tmp][0]]--;
    	}
    	Dfs_calc(1,0);
    	for(int i=1;i<=n;i++)
    		maxx=max(maxx,num[i]);
    	cout<<maxx<<'\n';
    	return 0;
    }
    
    边差分

    \(u,v\) 路径上每个边权加上 \(val\),把每条边权记在深度更大的节点上:

    \(diff_u += val,diff_v += val,diff_{lca} -= 2 \times val\)

    注意此时 \(lca\) 上存的边权并不在 \(u,v\) 路径包含范围内,故不应统计在内,同时如果用树链剖分计算路径长度,那么当 \(u,v\) 跳到同一条链之后的查询应做出改变,不包含更浅的节点上存的值

    P2680 [NOIP 2015 提高组] 运输计划

    Code
    #include<iostream>
    #include<cstring>
    #define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
    using namespace std;
    const int N=3e5+5;
    int n,m,head[N],nxt[N<<1],to[N<<1],w[N<<1],cnt=0,dep[N],father[N],siz[N],son[N],dfn[N],seg[N],top[N],idx=0,val[N],diff[N],sum[N],maxx,ans;
    struct Data
    {
    	int st,ed,co;
    	Data(int _st=0,int _ed=0,int _co=0):st(_st),ed(_ed),co(_co){}
    }a[N];
    struct Segment_Tree
    {
    	int Tree[N<<2];
    	void Pushup(int k)
    	{
    		Tree[k]=Tree[k<<1]+Tree[k<<1|1];
    	}
    	void Buildtree(int k,int L,int R)
    	{
    		if(L==R)
    		{
    			Tree[k]=val[seg[L]];
    			return;
    		}
    		int Mid=(L+R)>>1;
    		Buildtree(k<<1,L,Mid);
    		Buildtree(k<<1|1,Mid+1,R);
    		Pushup(k);
    	}
    	int Query(int k,int L,int R,int ql,int qr)
    	{
    		if(ql>qr) return 0;
    		if(ql<=L&&R<=qr)
    			return Tree[k];
    		int Mid=(L+R)>>1,res=0;
    		if(ql<=Mid)
    			res=res+Query(k<<1,L,Mid,ql,qr);
    		if(qr>Mid)
    			res=res+Query(k<<1|1,Mid+1,R,ql,qr);
    		return res;
    	}
    }Segment;
    void Dfs_pre(int u,int fa)
    {
    	dep[u]=dep[fa]+1,father[u]=fa,siz[u]=1;
    	for(int i=head[u];i;i=nxt[i])
    	{
    		int v=to[i];
    		if(v==fa) continue;
    		val[v]=w[i];
    		Dfs_pre(v,u);
    		siz[u]+=siz[v];
    		if(siz[v]>siz[son[u]]) son[u]=v;
    	}
    }
    void Dfs_split(int u,int new_top)
    {
    	if(!u) return;
    	dfn[u]=++idx,seg[idx]=u,top[u]=new_top;
    	Dfs_split(son[u],new_top);
    	for(int i=head[u];i;i=nxt[i])
    	{
    		int v=to[i];
    		if(v==father[u]||v==son[u]) continue;
    		Dfs_split(v,v);
    	}
    }
    int Query_path(int u,int v)
    {
    	int res=0;
    	while(top[u]!=top[v])
    	{
    		if(dep[top[u]]>=dep[top[v]])
    			res=res+Segment.Query(1,1,n,dfn[top[u]],dfn[u]),u=father[top[u]];
    		else
    			res=res+Segment.Query(1,1,n,dfn[top[v]],dfn[v]),v=father[top[v]];
    	}
    	if(dep[u]>=dep[v])
    		res=res+Segment.Query(1,1,n,dfn[v]+1,dfn[u]);//注意此处必须+1,因为v上的点权所对应的边不在v->u之内
    	else
    		res=res+Segment.Query(1,1,n,dfn[u]+1,dfn[v]);
    	return res;
    }
    int LCA(int u,int v)
    {
    	while(top[u]!=top[v])
    	{
    		if(dep[top[u]]>=dep[top[v]])
    			u=father[top[u]];
    		else
    			v=father[top[v]];
    	}
    	if(dep[u]>=dep[v])
    		return v;
    	else
    		return u;
    }
    void Add(int u,int v,int cost)
    {
    	to[++cnt]=v;
    	w[cnt]=cost;
    	nxt[cnt]=head[u];
    	head[u]=cnt;
    }
    void Dfs_calc(int u,int fa)
    {
    	sum[u]+=diff[u];
    	for(int i=head[u];i;i=nxt[i])
    	{
    		int v=to[i];
    		if(v==fa) continue;
    		Dfs_calc(v,u);
    		sum[u]+=sum[v];
    	}
    }
    int main()
    {
    	IOS;
    	cin>>n>>m;
    	for(int i=1;i<n;i++)
    	{
    		int u,v,w;
    		cin>>u>>v>>w;
    		Add(u,v,w),Add(v,u,w);
    	}
    	Dfs_pre(1,0);
    	Dfs_split(1,1);
    	Segment.Buildtree(1,1,n);
    	for(int i=1;i<=m;i++)
    	{
    		int s,t,tmp;
    		cin>>s>>t;
    		tmp=Query_path(s,t);
    		maxx=max(maxx,tmp);
    		a[i]={s,t,tmp};
    	}
    	int L=0,R=maxx+1;
    	while(L<=R)
    	{
    		memset(diff,0,sizeof(diff));
    		memset(sum,0,sizeof(sum));
    		int Mid=(L+R)>>1,tot=0;
    		for(int i=1;i<=m;i++)
    		{
    			if(a[i].co>Mid)
    			{
    				tot++;
    				diff[a[i].st]+=1,diff[a[i].ed]+=1;
    				diff[LCA(a[i].st,a[i].ed)]-=2;
    			}
    		}
    		if(tot==0)
    		{
    			ans=Mid,R=Mid-1;
    			continue;
    		}
    		Dfs_calc(1,0);
    		bool flag=false;
    		for(int i=1;i<=n;i++)
    		{
    			if(sum[i]==tot&&maxx-val[i]<=Mid)
    			{
    				flag=true;
    				break;
    			}
    		}
    		if(flag)
    			ans=Mid,R=Mid-1;
    		else
    			L=Mid+1;
    	}
    	cout<<ans<<'\n';
    	return 0;
    }
    

ST表

  1. 静态区间最值/区间GCD等问题。运算对象需满足可并性,且重叠不影响求解。

    详解 用倍增的思想解决可重复贡献的区间最值/GCD/LCM/按位与/按位或问题

    P3865 【模板】ST 表 & RMQ 问题

    Code
    #include<iostream>
    #define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
    using namespace std;
    const int N=1e5+5;
    const int LogN=20;
    int n,m,a[N],f[N][LogN],Log[N];
    int main()
    {
    	IOS;
    	cin>>n>>m;
    	Log[0]=-1;
    	for(int i=1;i<=n;i++)
    		cin>>a[i],Log[i]=Log[i>>1]+1,f[i][0]=a[i];
    	for(int j=1;j<=Log[n];j++)
    		for(int i=1;i+(1<<j)-1<=n;i++)
    			f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);
    	while(m--)
    	{
    		int L,R;
    		cin>>L>>R;
    		int k=Log[R-L+1];
    		cout<<max(f[L][k],f[R-(1<<k)+1][k])<<'\n';
    	}
    	return 0;
    }
    

树状数组

在线单次 \(O(\log n)\) 维护区间和等具有逆运算的区间答案。

通过加一个log的复杂度可以维护区间最值等问题。

线段树

  1. 对满足幺半群的运算会建立线段树。

  2. 会对多标记的优先级进行分析。

    P3373 【模板】线段树 2

    Code
    #include<iostream>
    #define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
    using namespace std;
    const int N=1e5+5;
    int n,Q,MOD,a[N];
    struct Segment_tree
    {
    	long long Tree[N<<2],add_tag[N<<2],mul_tag[N<<2];
    	void Pushup(int k)
    	{
    		Tree[k]=(Tree[k<<1]+Tree[k<<1|1])%MOD;
    	}
    	void Buildtree(int k,int L,int R)
    	{
    		mul_tag[k]=1;
    		if(L==R)
    		{
    			Tree[k]=a[L]%MOD;
    			return;
    		}
    		int Mid=(L+R)>>1;
    		Buildtree(k<<1,L,Mid);
    		Buildtree(k<<1|1,Mid+1,R);
    		Pushup(k);
    	}
    	void Pushdown(int k,int L,int R)
    	{
    		int Mid=(L+R)>>1;
    		if(mul_tag[k]!=1)
    		{
    			Tree[k<<1]=Tree[k<<1]*mul_tag[k]%MOD;
    			mul_tag[k<<1]=mul_tag[k<<1]*mul_tag[k]%MOD;
    			add_tag[k<<1]=add_tag[k<<1]*mul_tag[k]%MOD;
    			Tree[k<<1|1]=Tree[k<<1|1]*mul_tag[k]%MOD;
    			mul_tag[k<<1|1]=mul_tag[k<<1|1]*mul_tag[k]%MOD;
    			add_tag[k<<1|1]=add_tag[k<<1|1]*mul_tag[k]%MOD;
    		}
    		if(add_tag[k]!=0)
    		{
    			Tree[k<<1]=(Tree[k<<1]+add_tag[k]*(Mid-L+1)%MOD)%MOD;//记得乘上区间长度
    			add_tag[k<<1]=(add_tag[k<<1]+add_tag[k])%MOD;
    			Tree[k<<1|1]=(Tree[k<<1|1]+add_tag[k]*(R-Mid)%MOD)%MOD;
    			add_tag[k<<1|1]=(add_tag[k<<1|1]+add_tag[k])%MOD;
    		}
    		mul_tag[k]=1,add_tag[k]=0;
    	}
    	void Modify_add(int k,int L,int R,int ql,int qr,long long val)
    	{
    		if(ql<=L&&R<=qr)
    		{
    			Tree[k]=(Tree[k]+val*(R-L+1)%MOD)%MOD;
    			add_tag[k]=(add_tag[k]+val)%MOD;
    			return;
    		}
    		Pushdown(k,L,R);
    		int Mid=(L+R)>>1;
    		if(ql<=Mid)
    			Modify_add(k<<1,L,Mid,ql,qr,val);
    		if(qr>Mid)
    			Modify_add(k<<1|1,Mid+1,R,ql,qr,val);
    		Pushup(k);
    	}
    	void Modify_mul(int k,int L,int R,int ql,int qr,long long val)
    	{
    		if(ql<=L&&R<=qr)
    		{
    			Tree[k]=Tree[k]*val%MOD;
    			mul_tag[k]=mul_tag[k]*val%MOD;
    			add_tag[k]=add_tag[k]*val%MOD;
    			return;
    		}
    		Pushdown(k,L,R);
    		int Mid=(L+R)>>1;
    		if(ql<=Mid)
    			Modify_mul(k<<1,L,Mid,ql,qr,val);
    		if(qr>Mid)
    			Modify_mul(k<<1|1,Mid+1,R,ql,qr,val);
    		Pushup(k);
    	}
    	long long Query(int k,int L,int R,int ql,int qr)
    	{
    		if(ql<=L&&R<=qr)
    			return Tree[k];
    		Pushdown(k,L,R);
    		long long res=0;
    		int Mid=(L+R)>>1;
    		if(ql<=Mid)
    			res=(res+Query(k<<1,L,Mid,ql,qr))%MOD;
    		if(qr>Mid)
    			res=(res+Query(k<<1|1,Mid+1,R,ql,qr))%MOD;
    		return res;
    	}
    }Segment;
    int main()
    {
    	IOS;
    	cin>>n>>Q>>MOD;
    	for(int i=1;i<=n;i++)   
    		cin>>a[i];
    	Segment.Buildtree(1,1,n);
    	while(Q--)
    	{
    		int opt,L,R;
    		long long val;
    		cin>>opt;
    		if(opt==1)
    			cin>>L>>R>>val,Segment.Modify_mul(1,1,n,L,R,val);
    		else if(opt==2)
    			cin>>L>>R>>val,Segment.Modify_add(1,1,n,L,R,val);
    		else
    			cin>>L>>R,cout<<Segment.Query(1,1,n,L,R)<<'\n';
    	}
    	return 0;
    }
    
  3. 会进行线段树上二分。

  4. 会使用权值线段树,会离散化后维护。

    P3369 【模板】普通平衡树

    Code
    #include<iostream>
    #include<algorithm>
    #define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
    using namespace std;
    const int N=1e5+5;
    struct Segment_Tree
    {
    	int Tree[N<<2];
    	void Pushup(int k)
    	{
    		Tree[k]=Tree[k<<1]+Tree[k<<1|1];
    	}
    	void Modify(int k,int L,int R,int pos,int val)
    	{
    		if(L==R)
    		{
    			Tree[k]+=val;
    			return;
    		}
    		int Mid=(L+R)>>1;
    		if(pos<=Mid)
    			Modify(k<<1,L,Mid,pos,val);
    		if(pos>Mid)
    			Modify(k<<1|1,Mid+1,R,pos,val);
    		Pushup(k);
    	}
    	int Query(int k,int L,int R,int ql,int qr)
    	{
    		if(ql>qr) return 0;//注意边界问题
    		if(ql<=L&&R<=qr)
    			return Tree[k];
    		int res=0,Mid=(L+R)>>1;
    		if(ql<=Mid)
    			res=res+Query(k<<1,L,Mid,ql,qr);
    		if(qr>Mid)
    			res=res+Query(k<<1|1,Mid+1,R,ql,qr);
    		return res;
    	} 
    
    	int Query_val(int k,int L,int R,int rank)
    	{
    		if(L==R) return L;
    		int Mid=(L+R)>>1;
    		if(rank<=Tree[k<<1])
    			return Query_val(k<<1,L,Mid,rank);
    		else
    			return Query_val(k<<1|1,Mid+1,R,rank-Tree[k<<1]);
    	}
    	int Predecessor(int siz,int val)
    	{
    		int cnt=Query(1,1,siz,1,val-1);
    		if(cnt==0) return 0;
    		return Query_val(1,1,siz,cnt);
    	}
    	int Successor(int siz,int val)
    	{
    		int cnt=Query(1,1,siz,1,val);
    		if(cnt==Tree[1]) return 0;
    		return Query_val(1,1,siz,cnt+1);
    	}
    }Segment;
    struct Node
    {
    	int opt;
    	int x;
    }Ques[N];
    int n,siz,Hash[N];
    int main()
    {
    	IOS;
    	cin>>n;
    	for(int i=1;i<=n;i++)
    	{
    		cin>>Ques[i].opt>>Ques[i].x;
    		if(Ques[i].opt!=4) Hash[++siz]=Ques[i].x;
    	}
    	sort(Hash+1,Hash+siz+1);
    	siz=unique(Hash+1,Hash+siz+1)-(Hash+1);
    	for(int i=1;i<=n;i++)
    	{
    		if(Ques[i].opt==4) continue;
    		Ques[i].x=lower_bound(Hash+1,Hash+siz+1,Ques[i].x)-Hash;
    	}
    	for(int i=1;i<=n;i++)
    	{
    		if(Ques[i].opt==1)
    			Segment.Modify(1,1,siz,Ques[i].x,1);
    		else if(Ques[i].opt==2)
    			Segment.Modify(1,1,siz,Ques[i].x,-1);
    		else if(Ques[i].opt==3)
    			cout<<Segment.Query(1,1,siz,1,Ques[i].x-1)+1<<'\n';
    		else if(Ques[i].opt==4)
    			cout<<Hash[Segment.Query_val(1,1,siz,Ques[i].x)]<<'\n';
    		else if(Ques[i].opt==5)
    			cout<<Hash[Segment.Predecessor(siz,Ques[i].x)]<<'\n';
    		else
    			cout<<Hash[Segment.Successor(siz,Ques[i].x)]<<'\n';
    	}
    	return 0;
    }
    
  5. 会动态开点线段树。

  6. 标记永久化:只需要打标记无需下放,标记只会增加或者只会擦除原有存在标记。

  7. 利用线段树维护二维偏序和扫描线。

  8. 会线段树优化DP。

  9. 会进行可持久化,并利用可持久化后的线段树进行版本diff后求解。

    P3834 【模板】可持久化线段树 2

    Code
    #include<iostream>
    #include<algorithm>
    #define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
    using namespace std;
    const int N=2e5+5;
    const int LogN=25;
    struct Segment_Tree
    {
    	struct Node
    	{
    		int lft,rgt,siz;
    		Node(){lft=0,rgt=0,siz=0;}
    	}Tree[(N<<2)+N*LogN];
    	int point=0;
    	int Clone(int k)
    	{
    		Tree[++point]=Tree[k];
    		return point;
    	}
    	void Pushup(int k)
    	{
    		Tree[k].siz=Tree[Tree[k].lft].siz+Tree[Tree[k].rgt].siz;
    	}
    	int Buildtree(int k,int L,int R)
    	{
    		k=Clone(k);
    		if(L==R) return k;
    		int Mid=(L+R)>>1;
    		Tree[k].lft=Buildtree(Tree[k].lft,L,Mid);
    		Tree[k].rgt=Buildtree(Tree[k].rgt,Mid+1,R);
    		Pushup(k);
    		return k;
    	}
    	int Modify(int k,int L,int R,int pos,int val)
    	{
    		k=Clone(k);
    		if(L==R)
    		{
    			Tree[k].siz+=val;
    			return k;
    		}
    		int Mid=(L+R)>>1;
    		if(pos<=Mid)
    			Tree[k].lft=Modify(Tree[k].lft,L,Mid,pos,val);
    		if(pos>Mid)
    			Tree[k].rgt=Modify(Tree[k].rgt,Mid+1,R,pos,val);
    		Pushup(k);
    		return k;
    	}
    	int Query(int A,int B,int L,int R,int rank)
    	{
    		if(L==R) 
    			return L;
    		int Mid=(L+R)>>1,lftsiz=Tree[Tree[B].lft].siz-Tree[Tree[A].lft].siz;
    		if(rank<=lftsiz)
    			return Query(Tree[A].lft,Tree[B].lft,L,Mid,rank);
    		else
    			return Query(Tree[A].rgt,Tree[B].rgt,Mid+1,R,rank-lftsiz);
    	}
    }Segment;
    int n,m,a[N],Hash[N],siz,root[N];
    int main()
    {
    	IOS;
    	cin>>n>>m;
    	for(int i=1;i<=n;i++)
    		cin>>a[i],Hash[i]=a[i];
    	sort(Hash+1,Hash+n+1);
    	siz=unique(Hash+1,Hash+n+1)-Hash-1;
    	root[0]=Segment.Buildtree(0,1,siz);
    	for(int i=1;i<=n;i++)
    	{
    		a[i]=lower_bound(Hash+1,Hash+siz+1,a[i])-Hash;
    		root[i]=Segment.Modify(root[i-1],1,siz,a[i],1);
    	}
    	while(m--)
    	{
    		int L,R,k;
    		cin>>L>>R>>k;
    		cout<<Hash[Segment.Query(root[L-1],root[R],1,siz,k)]<<'\n';
    	}
    	return 0;
    }
    
  10. 会进行线段树合并。

平衡树

  1. 会写无旋treap,会求解第k大问题,会解决区间翻转标记。
  2. 会写可持久化treap。

cdq分治

  1. 会利用cdq分治解决二维偏序和三维偏序。

分块

  1. 对序列分块
  2. 对操作分块(每根号n个操作后更新当前操作对象)
  3. 块间\(O(n\sqrt{n})\)预处理(区间众数)
  4. 会使用根号平衡将问题分解为两种情形并分别维护。

莫队

  1. 普通莫队
  2. 带修莫队

bitset

  1. bitset维护图上传递闭包。
  2. bitset维护01背包。
  3. bitset维护状态压缩。

树链剖分

  1. 会进行轻重链剖分,并可基于dfs树将路径分解为\(O(\log n)\)个链更新。

并查集

  1. 会普通并查集。并会进行路径压缩。
  2. 会带权并查集。会维护两种关系:集合本身信息/点到集合代表元素间的信息。
  3. 会拆点维护多重并查集/扩展域并查集。
  4. 会维护关系并查集。
  5. 基于并查集维护生成树。
  6. 会维护最小生成树重构树,并理解节点含义,能对重构树维护树上DP或查询节点关系。
  7. 若不能路径压缩,会写按秩合并。
  8. 会写带撤销并查集。
  9. 会写可持久化并查集。

最近公共祖先

  1. 基于倍增或树链剖分维护最近公共祖先。
  2. 基于倍增或树链剖分维护链上信息。

字符串相关

  1. 会写KMP。
  2. 会写字典树,包括普通字典树和01字典树。
  3. 基于01字典树维护异或最值。
  4. 会写可持久化字典树。
  5. 会写马拉车求回文串。

哈希

  1. 会基于进制的哈希。
  2. 会基于异或的哈希。
  3. 会基于随机化与和的哈希。
  4. 会对二叉树、栈上状态等进行哈希。
  5. 会基于哈希求回文串、基于哈希判断状态相同。
posted @ 2025-10-23 20:18  FallingGardenia  阅读(8)  评论(0)    收藏  举报