各种算法及体会

单调栈/队列

单调栈用于求解该点左/右侧首个大/小于该点的位置(注意!不需要用什么线段树啊二分啊

注意有时在用单调栈解决区间问题时可能会重复计算,故把左右单调栈一个小于,一个小于等于即可!

单调队列即滑动窗口,可以求解定区间最大/小值

最长上升子序列(LIS)

O(nlogn)维护方法,考虑二分,每次用更小值替换之前的位置,大于结尾的加入vector,最后的size即所求。

最长公共子序列(LCS)可以转化为这个问题。

    for(int i=1;i<=n;i++){//O(nlogn) LIS
		int x=lower_bound(v.begin(),v.end(),a[i])-v.begin();
		if(x==v.size()) v.push_back(a[i]);
		else v[x]=a[i];
	}

ST表

用于维护RMQ问题,即区间最大/小值。不能修改,但O(1)查询。(欧拉序+st表的lca经常用来卡常)

void ST(){
	lg[0]=-1;
	for(int i=1;i<=n;i++){
		f[i][0]=h[i],lg[i]=lg[i>>1]+1;
	}
	for(int j=1;(1<<j)<=n;j++){
		for(int i=1;i+(1<<j)-1<=n;i++){
			f[i][j]=min(f[i][j-1],f[i+(1<<(j-1))][j-1]);
		}
	}
}
int query(int l,int r){
	int k=lg[r-l+1];
	return min(f[l][k],f[r-(1<<k)+1][k]);
}

分块

O(n根n)

//分块 
#include<bits/stdc++.h>
using namespace std;
int opt,x,y,c,st[100005],ed[100005],a[100005],pos[100005],len,n,m,cnt;
long long sum[100005],add[100005];
struct block{
	void build(){
		len=(int)sqrt(n);
		cnt=n/len;
		if(n%len) cnt++;
		for(int i=1;i<=cnt;i++){
			st[i]=(i-1)*len+1;
			ed[i]=i*len;
		}
		ed[cnt]=n;
		for(int i=1;i<=cnt;i++){
			for(int j=st[i];j<=ed[i];j++){
				sum[i]+=a[j];
				pos[j]=i;
			}
		}
	}
	void update(int l,int r,long long k){
		if(pos[l]==pos[r]){
			for(int i=l;i<=r;i++){
				a[i]+=k;
			}
			sum[pos[l]]+=(r-l+1)*k;
		}
		else {
			for(int i=pos[l]+1;i<pos[r];i++){
				add[i]+=k;
			}
			for(int i=l;i<=ed[pos[l]];i++){
				a[i]+=k;
			}
			sum[pos[l]]+=(ed[pos[l]]-l+1)*k;
			for(int i=st[pos[r]];i<=r;i++){
				a[i]+=k;
			}
			sum[pos[r]]+=(r-st[pos[r]]+1)*k;
		}
	}
	long long query(int l,int r){
		long long ans=0;
		if(pos[l]==pos[r]){
			for(int i=l;i<=r;i++){
				ans+=a[i];
			}
			ans+=(r-l+1)*add[pos[l]];
		}
		else {
			for(int i=pos[l]+1;i<pos[r];i++){
				ans+=sum[i]+add[i]*(ed[i]-st[i]+1);
			}
			for(int i=l;i<=ed[pos[l]];i++){
				ans+=a[i];
			}
			ans+=add[pos[l]]*(ed[pos[l]]-l+1);
			for(int i=st[pos[r]];i<=r;i++){
				ans+=a[i];
			}
			ans+=add[pos[r]]*(r-st[pos[r]]+1);
		}
		return ans;
	}
};
block B;
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	B.build();
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d%d",&opt,&x,&y);
		if(opt==1){
			long long k;
			scanf("%lld",&k);
			B.update(x,y,k);
		}
		else {
			printf("%lld\n",B.query(x,y));
		}
	}
	return 0;
}

莫队

O(n根n)左右

//带修莫队
#include<bits/stdc++.h>
using namespace std;
int n,m,ans,out[140005],c[140005],pos[140005],l=1,r,now,s[1000005],an,bn;
struct node{
	int v,l,r,id,t;
}a[140005],b[140005];
bool cmp(const node &p,const node &q){//重在排序
	if(pos[p.l]!=pos[q.l]) return p.l<q.l;
	if(pos[p.r]!=pos[q.r]) return p.r<q.r;
	return p.t<q.t;
}
void add(int x){
	s[c[x]]++;
	if(s[c[x]]==1) ans++;
}
void del(int x){
	s[c[x]]--;
	if(s[c[x]]==0) ans--;
}
void change(int now,int x){
	if(b[now].l>=a[x].l&&b[now].l<=a[x].r){
		s[c[b[now].l]]--;
		s[b[now].r]++;
		if(s[c[b[now].l]]==0) ans--;
		if(s[b[now].r]==1) ans++;
	}
	swap(b[now].r,c[b[now].l]);
}
int main(){
	scanf("%d%d",&n,&m);
	int len=(int)pow(n,0.666);//带修莫队这个块长最快
	for(int i=1;i<=n;i++){
		scanf("%d",&c[i]);
		pos[i]=(i-1)/len+1;
	}
	for(int i=1;i<=m;i++){
		char opt;
		scanf(" %c",&opt);
		if(opt=='Q') {
			an++;
			a[an].id=an;
			a[an].t=bn;
			scanf("%d%d",&a[an].l,&a[an].r);
		}
		else if(opt=='R'){
			bn++;
			scanf("%d%d",&b[bn].l,&b[bn].r);//l支笔,r颜色 
		}
	}
	sort(a+1,a+an+1,cmp);
	for(int i=1;i<=an;i++){//
		while(l<a[i].l) del(l++);
		while(l>a[i].l) add(--l);
		while(r<a[i].r) add(++r);
		while(r>a[i].r) del(r--);
		while(now<a[i].t) change(++now,i);
		while(now>a[i].t) change(now--,i);
		out[a[i].id]=ans;
	}
	for(int i=1;i<=an;i++){///
		printf("%d\n",out[i]);
	}
	return 0;
}

基环树

最常见条件:n个点,n条边(注意题目给的条件是否联通,可能是森林

基环内向树:每个点只有一条出边

基环外向树:每个点只有一条入边

处理手法:找环,断边,跑树形dp

void circle(int x,int p){
	vis[x]=1;
	for(int i=h[x];i;i=e[i].nxt){
		int y=e[i].to;
		if((i^1)==p) continue;
		if(vis[y]){
			xx=x,yy=y,k=i;
			continue;//不能return,要把联通块找完 
		}
		circle(y,i);
	}
}

扫描线

求矩形面积并,周长,二维数点等等

线段树作用即把静态O(n^2)变为动态O(nlogn).

想象过程,就是在一张图上,一条线从上到下扫描。所以线段树本质维护的是矩形的长,而宽又是从上到下排序过的,所以每次乘上差值即可。

常用手法比如把点的问题转化为矩形,可以求最大面积交/并

注意这个线段树写法和别的不太一样,有时需要离散化或动态开点。而且记录询问的数组开2倍。线段树必须开16倍空间,读入add就是二倍,线段树4倍,在pushup不可避免访问叶子节点的儿子,再有2倍。

#include<bits/stdc++.h>
#define ll long long
#define lid (id<<1)
#define rid (id<<1|1)
using namespace std;
const int maxn=100015;
struct node{
	ll x,yl,yr;
	int fl;
}p[maxn<<1];
struct tree{
	ll l,r,sum;
}t[maxn<<4];
int n,tg[maxn<<4];
ll s[maxn<<1],ans;
bool cmp(node a,node b){
	return a.x<b.x;
}
void pushup(int id){
	if(tg[id]>0) t[id].sum=t[id].r-t[id].l;//注意,这里代表是否矩形完全覆盖该位置 
	else t[id].sum=t[lid].sum+t[rid].sum;
}
void build(int id,int l,int r){
	t[id].l=s[l];//离散化,t[id]的l/r不是普遍意义 
	t[id].r=s[r];
	if(r-l==1){
		t[id].sum=0;
		return ;
	}
	int mid=(l+r)>>1;
	build(lid,l,mid);//维护的是面积,所以是按照矩形的边计算,mid相同 
	build(rid,mid,r);
	pushup(id);
}
void add(int id,ll yl,ll yr,int fl){
	if(t[id].l==yl&&t[id].r==yr){
		tg[id]+=fl;
		pushup(id);
		return;
	}//这种写法实际上mid=t[lid].r=t[rid].l 
	if(yl<t[lid].r) add(lid,yl,min(t[lid].r,yr),fl);
	if(yr>t[rid].l) add(rid,max(t[rid].l,yl),yr,fl);
	pushup(id);
} 
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		ll xl,yl,xr,yr;
		scanf("%lld%lld%lld%lld",&xl,&yl,&xr,&yr);
		p[i]=(node){xl,yl,yr,1};
		p[i+n]=(node){xr,yl,yr,-1};
		s[i]=yl,s[i+n]=yr;
	}
	sort(s+1,s+n*2+1);
	sort(p+1,p+n*2+1,cmp);
	build(1,1,2*n);
	add(1,p[1].yl,p[1].yr,p[1].fl);
	for(int i=2;i<=2*n;i++){
		ans+=(p[i].x-p[i-1].x)*t[1].sum;
	//printf("%d ",i);
		add(1,p[i].yl,p[i].yr,p[i].fl);
	}
	printf("%lld",ans);
	return 0;
}

网络流

最大流=最小割

O(n^2m)但玄学上界,一般都能跑。。

推这个,怎么说,不一定能看出来是网络流,甚至可能是把一个数据类的抽象到图论上。总之很巧妙,像dp。就先看限制约束条件,可以先思考连边,然后考虑是最大流、最小割还是什么总体-局部的转换,最后建图吧?

有一个最大权闭合子图,套路性的正边-源点,负边-汇点,答案为正边权和-最小割。

求解一条边能否在最小割和是否一定在最小割中的问题时,考虑在最终的残量网络上用 tarjan 求解。

void add(int u,int v,int val){
	nxt[++cnt]=h[u],to[cnt]=v,h[u]=cnt,w[cnt]=val;
	nxt[++cnt]=h[v],to[cnt]=u,h[v]=cnt,w[cnt]=0; //有向图! 
}
int bfs(){
	memset(dep,0,sizeof(dep));
	queue<int>q;
	q.push(s);
	dep[s]=1;
	now[s]=h[s];
	while(!q.empty()){
		int x=q.front();
		q.pop();
		for(int i=h[x];i;i=nxt[i]){
			int y=to[i];
			if(w[i]>0&&dep[y]==0){//直接考虑能走的边 
				q.push(y);
				now[y]=h[y];//当前弧优化 
				dep[y]=dep[x]+1;//分层图 
				if(y==t) return 1;
			}
		}
	}
	return 0;
}
int dfs(int x,int sum){//这里,sum对整条路的最大贡献流量 
	if(x==t) return sum;
	int k,res=0;//k当前最小剩余流量 
	for(int i=now[x];i&&sum;i=nxt[i]){
		now[x]=i;//当前弧优化,走过的 
		int y=to[i];
		if(w[i]>0&&(dep[y]==dep[x]+1)){//保证是最短路 
			k=dfs(y,min(sum,w[i]));
			if(k==0) dep[y]=0;//剪枝,增广完毕 
			w[i]-=k,w[i^1]+=k;//正反向边 
			res+=k;//经过该点的流量和 
			sum-=k;//经过该点当前剩余流量 
		}
	}
	return res;
}
void dinic(){
    while(bfs()){
        ans+=dfs(s,inf);
    }
}

费用流

保证最大流的同时费用最大/小

O(n2m2)

void add(int u,int v,ll flow,ll val){
	nxt[++cnt]=h[u],to[cnt]=v,h[u]=cnt,w[cnt]=val,f[cnt]=flow;
	nxt[++cnt]=h[v],to[cnt]=u,h[v]=cnt,w[cnt]=-val,f[cnt]=0;
}
queue<int>q;
bool SPFA(){
	memset(dis,-0x3f,sizeof(dis));
	memset(fl,0x3f,sizeof(fl));
	memset(vis,0,sizeof(vis));
	q.push(s); vis[s]=1,dis[s]=0,pre[t]=-1;
	while(!q.empty()){
		int x=q.front();
		q.pop();
		vis[x]=0;
		for(int i=h[x];i;i=nxt[i]){
			int y=to[i];
			if(f[i]>0&&dis[x]+w[i]>dis[y]){
				dis[y]=dis[x]+w[i];
				pre[y]=x;
				lst[y]=i;
				fl[y]=min(fl[x],f[i]);
				if(!vis[y]){ vis[y]=1;q.push(y); }
			}
		}
	}
	return pre[t]!=-1;
}
void EK(){
	while(SPFA()){
		int x=t;
		if(res+dis[t]*fl[t]<0){
			ans+=(ll)res/-dis[t]; 
			return ;
		} 
		ans+=fl[t];
		res+=dis[t]*fl[t];
		while(x!=s){
			f[lst[x]]-=fl[t];
			f[lst[x]^1]+=fl[t];
			x=pre[x];
		}
	} 
}

上下界网络流(可行、最大、最小、费用流)

主要思想就是先让它直接流下界的流量,然后连汇点t到源点s(因为流量应该相等),多余或不足的流量从新的源点汇点S、T补,然后跑最大流,如果把新源点汇点的边跑满,则可行。

最大流就是在残量网络上s-t跑最大流+可行流。
最小流就是删去和S、T连边以及t-s后跑最大流,w(t-s)-最大流。
费用流基本是可行费用流,套板子即可。

这个算法真的,不太用动脑(?只要能用的,按题意模拟即可。(算是网络流中最好想的吧。。)

一些tips

1,如果求类似最大值最小,考虑和二分答案相结合,用最大流、可行流check等等

2,与源汇点、二分图有关三大套路:格子染色(用于仅与相邻有关的)、数学分奇偶性、矩阵分横纵以点连线

3,拆点,每个状态为一个点或入边->出边,中间是限制条件。点数太大了考虑动态加点。

4,任意两边的交点都在顶点上的图称为平面图,如果我们把面作为点,相邻的边连线,可以得到对偶图,对于数据点过大的,平面图最小割=对偶图最短路。

5,分数规划,其实就是把分母乘过去,然后二分验证。

6,对于平方的处理:拆边,假设全一种情况,考虑增量;或根据2k+1来连。

·如果不刻意卡,6e3-1e5的点数甚至都能过去!网络流就是要敢写!!!

后缀数组

用于解决字符串相关问题。倍增法+基数排序O(nlogn),考虑每次把2^(k-1)的两个字符串合并,遇到结束时用空串补足。

sa[i]后缀数组,记录排名为i的后缀在原串中的位置,sa[排名]=位置,rk[位置]=排名。

height[i]=suffix(sa[i-1])和suffix(sa[i])的最长公共前缀(排名相邻的两个后缀的最长公共前缀),h[i]=height[rk[i]]也就是suffix(i)和它前一名后缀的最长公共前缀。(即LCP,最长公共前缀)

注意性质h[i]>=h[i-1]-1,考虑设suffix(k)是suffix(i-1)前一名的后缀,则最长公共前缀为h[i-1]。那么suffix(k+1)排在suffix(i)前,理解上同减一位即可,又考虑到还可能有前几位相同但比suffix(k+1)长的串夹在二者中间(感性理解一下),故得证。(注:不是严谨证明,只是便于理解)

这个性质可以O(n)求h[i],而对于询问任意两个后缀的最长公共前缀,答案几位两者排名之间的height最小值,RMQ问题用ST表维护即可。height数组的特性非常好用,不重不漏,还一定包含公共子串的最值!!!

(真服了,一个板子看了一个下午。。。)

//后缀排序,后缀数组基础,用于字符串问题
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e6+10;//特别注意,数组开二倍!因为有+k
int n,m,x[maxn],y[maxn],c[maxn],sa[maxn],rk[maxn],height[maxn];
char s[maxn];
void getsa(){
	//按第一个字母排序 
	for(int i=1;i<=n;i++) c[x[i]=s[i]]++;//桶 
	for(int i=1;i<=m;i++) c[i]+=c[i-1];
	for(int i=n;i>0;i--) sa[c[x[i]]--]=i;
	for(int k=1;k<=n;k<<=1){
		//按第二关键字排序 
		for(int i=0;i<=m;i++) c[i]=0;
		for(int i=1;i<=n;i++) y[i]=sa[i];//记录当前某位对应的原位置 
		for(int i=1;i<=n;i++) c[x[y[i]+k]]++;
		for(int i=1;i<=m;i++) c[i]+=c[i-1];//记录某数有若干个,便于排序(注意0的存在,所以从1开始 
		for(int i=n;i>0;i--) sa[c[x[y[i]+k]]--]=y[i];//按第二位排,注意对应从大到小的顺序 
		//按第一关键字排序
		for(int i=0;i<=m;i++) c[i]=0;
		for(int i=1;i<=n;i++) y[i]=sa[i];
		for(int i=1;i<=n;i++) c[x[y[i]]]++;
		for(int i=1;i<=m;i++) c[i]+=c[i-1];
		for(int i=n;i>0;i--) sa[c[x[y[i]]]--]=y[i];//首位排 
		//把后缀放入桶
		for(int i=1;i<=n;i++) y[i]=x[i];//原字符串 
		m=0;
		for(int i=1;i<=n;i++){//相对大小即可 
			if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]) x[sa[i]]=m;
			else x[sa[i]]=++m;
		} 
		if(m==n) break;//均不相同即为完成 
	}
}
//height[i]为suffix(sa[i-1]和sa[i])最长公共前缀,h[i]为suffix(i)和它前一名后缀的最长公共前缀 
void geth(){//性质:h[i]>=h[i-1]-1 
	for(int i=1;i<=n;i++) rk[sa[i]]=i;
	for(int i=1,k=0;i<=n;i++){
		if(rk[i]==1) continue;//第一名height为0 
		if(k) k--;//上一个height-1 
		int j=sa[rk[i]-1];//前邻后缀j 
		while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;//性质,有点双指针的意思 
		height[rk[i]]=k;
	}
}
int main(){
	scanf("%s",s+1);
	n=strlen(s+1); 
	m=122;//'z'
	getsa();
	geth();
	for(int i=1;i<=n;i++) printf("%d ",sa[i]);
	printf("\n");
	for(int i=1;i<=n;i++) printf("%d ",height[i]);
	return 0;
} 

字符串处理技巧

1,断环为链。对于循环串,直接复制到后面。对于回文串,反向复制到后面,在中间加一个大于所有串内ascii的。

2,考虑每个子串都是每个后缀的前缀,而height数组的特殊性质能够满足sum(height)排除全部重复子串,max(height)为最长重复子串。

3,对于非重复最长公共子串、k次重复子串等许多问题都考虑后缀数组,二分+按height划分层次判断,或者用单调队列维护区间最小值。

4,查询公共串时用ST表维护check,O(1)处理两个部分的公共串。

原根

考试见到的,反正以后还要学

由欧拉定理知,若 \((a,m)=1\) ,则 \(a^{\varphi (m)} \equiv 1 \pmod {m}\)

因此满足 \(a^n \equiv 1 \pmod{m}\)的最小正整数 \(n\) 存在,这个 \(n\) 称作 \(a\)\(m\) 的阶,记作 \(\delta_m(a)\)\(ord_m(a)\)

性质: $a,a^2 ,\cdots,a^{\delta_m(a)} $模 \(m\) 两两不同余。

\(a^n \equiv 1 \pmod m\),则 \(\delta_m(a)\mid n\).

原根

\(m \in \mathbf{N}^{*},g\in \mathbf{Z}\). 若 \((g,m)=1\),且 \(\delta_m(g)=\varphi(m)\),则称 \(g\) 为模 \(m\) 的原根。

\(g\) 满足 \(\delta_m(g) = \left| \mathbf{Z}_m^* \right| = \varphi(m).\)\(m\) 是质数时,我们有 \(g^i \bmod m,\,0 \lt i \lt m\) 的结果互不相同。

\(m \geqslant 3, (g,m)=1\),则 \(g\) 是模 \(m\) 的原根的充要条件是,对于 \(\varphi(m)\) 的每个素因数 \(p\),都有\(g^{\frac{\varphi(m)}{p}}\not\equiv 1\pmod m.\)

若一个数 \(m\) 有原根,则它原根的个数为 \(\varphi(\varphi(m)).\)

一个数 \(m\) 存在原根当且仅当 \(m=2,4,p^{\alpha},2p^{\alpha}\),其中 \(p\) 为奇素数,\(\alpha\in \mathbf{N}^{*}.\)

素数 \(p\) 的最小原根 \(g_p=O\left(p^{0.25+\epsilon}\right)\),其中 \(\epsilon>0\).因此暴力寻找最小原根复杂度是可以接受的。

posted @ 2024-07-02 08:07  STAR_light0420  阅读(21)  评论(0编辑  收藏  举报