[笔记]CSP-S 2025 第二轮 Final Review

Hope everything ok.

依照 NOI 大纲 2025 进行整理,删掉了一些考不到的内容。

数据结构

STL

deque(双端队列)

deque<int> q;
q.size();
q.empty();
q.clear();
q.front(),q.back();
q.emplace_back(x),q.emplace_front(x);//比 push 快
q.pop_back(),q.pop_front();
q.insert(it,x);
q.erase(it);
q[x];//O(1) 随机访问,小心越界
q.begin(),q.end();//正序
q.rbegin(),q.rend();//逆序

priority_queue(优先队列)

廊桥分配

priority_queue<int> q;//(默认)大根堆
priority_queue<int,vector<int>,less<int>> q;//大根堆
priority_queue<int,vector<int>,greater<int>> q;//小根堆
struct Node{
	int x,y;
	bool operator < (const Node _) const{
		return x==_.x?y<_.y:x<_.x;
	}
};
priority_queue<Node> q;//自定义比较函数下,大的先被取出
priority_queue<Node,vector<Node>,less<Node>> q;//与上面作用相同
struct Node{
	int x,y;
	bool operator > (const Node _) const{
		return x==_.x?y>_.y:x>_.x;
	}
};
priority_queue<Node,vector<Node>,greater<Node>> q;//自定义比较函数下,小的先被取出

set unordered_set

若想使用可重集,请写 multiset unordered_mumltiset

set<int> s;
s.size();
s.empty();
s.clear();
s.emplace(x);//和 insert 类似,但更快
s.find(x);//返回x所在位置的迭代器,找不到为s.end()
s.count(x);//返回x出现的次数,若为set则只有0,1两种取值
s.erase(x),s.erase(it);//可以用迭代器和值两种方法删除,返回删除元素的下一个
s.lower_bound(x),s.upper_bound(x);//返回迭代器
s.begin(),s.end();//正序
s.rbegin(),s.rend();//逆序

map unordered_map

unordered_map 可以用 gp_hash_table 上位替代。

注意 ma[x] 的访问方式,若不存在会创建元素。如果仅判断是否存在,请使用 countfind

map<int,int> ma;
ma.size();
ma.empty();
ma.clear();
ma.emplace(x);
ma.find(x);
ma.count(x);//若不存在不会创建
ma[x];//访问键x对应的值,若不存在会创建
ma.erase(x),ma.erase(it);//可以用迭代器和键两种方法删除,返回删除元素的下一个
s.lower_bound(x),s.upper_bound(x);//对键进行,返回迭代器
s.begin(),s.end();//正序
s.rbegin(),s.rend();//逆序

单调队列

滑窗 琪露诺 跳房子

for(int i=1;i<=n;i++){//滑窗min
	while(!q.empty()&&a[q.back()]>a[i]) q.pop_back();
	q.push_back(i);
	if(q.front()<=i-k) q.pop_front();
	if(i>=k) cout<<a[q.front()]<<" ";
}

ST 表

板子

int n,f[N][20];//连续访问内存较快,所以 n 一般放第一维
inline void init_st(){
	for(int i=1;i<=__lg(n);i++)
		for(int j=1;j+(1<<i)-1<=n;j++)
			f[j][i]=max(f[j][i-1],f[j+(1<<(i-1))][i-1]);
}
inline int getmx(int l,int r){
	int d=__lg(r-l+1);
	return max(f[l][d],f[r-(1<<d)+1][d]);
}

并查集 ~ Here

板子 食物链(带权/扩展域) Junk Mail(带删除)

//带权并查集(维护d,siz)
inline int find(int x){
	if(fa[x]==x) return x;
	int y=find(fa[x]);
	d[x]+=d[fa[x]];//路径压缩时更新d
	return fa[x]=y;
}
inline bool merge(int x,int y,int w){
	find(x),find(y);//放在前面,因为需要d值
	w+=d[y]-d[x];
	x=fa[x],y=fa[y];
	if(x==y) return !w;
	fa[x]=y,d[x]=w,siz[y]+=siz[x];
	return 1;
}

树的孩子兄弟表示法

应该用不到。所做的就是将多叉树转成二叉树,遵循“左孩子,右兄弟”的方法,即第 \(1\) 个子节点变成左子节点,其他子节点用链的形式连在这个左子节点的右子树中。

void conv(int pos){
	int prei=-1;
	for(auto i:G[pos]){
		if(prei==-1) lc[pos]=i;
		else rc[prei]=i;
		prei=i;
		conv(i);
	}
}

树状数组

板子 1 板子 2

通过维护差分序列,达到区修点查的效果。

也可以实现区修区查,以及树状数组上倍增。但是因为我没怎么用过,所以就不放了。

inline int lb(int x){return x&-x;}
struct BIT{
	int s[N];
	inline void chp(int x,int v){for(;x<=n;x+=lb(x)) s[x]+=v;}
	inline void qry(int x){int a=0;for(;x;x-=lb(x)) a+=s[x];return a;}
}bit;

线段树

板子 1 板子 1.5(动态开点) 板子 2(区乘区加) 扶苏的问题

注意处理操作间的关系。区乘区加要先乘后加,区赋区加要先赋后加。

标记永久化(Here)是一种不使用标记下放 / 上传技巧。修改时将标记保留在修改节点处,并直接处理出该节点的答案,查询时额外累加标记的贡献。

标记永久化可以减小常数和码量,可以省去动态开点线段树标记下放时的新建节点,使节点总数严格在 \(m\log V\) 以内,但是适用情况除区间加法外较有限。

动态开点线段树若不使用标记永久化,需要注意在标记下传时新建节点。有结论,每次区修最多涉及 \(4\log V\) 个节点,所以理论上空间要开到 \(m\log V\)\(4\) 倍(然鹅卡不满)。

作者还没有考虑清楚 \(4m\log V\) 是否为正确的下界,考后会加以修缮。现仅用于提醒读者,如果动态开点不使用标记永久化,至少需要开 \(4\) 倍。

//板子 1.5(不使用标记永久化 2.13s)
struct SEG{
	int s[M<<6],tg[M<<6],lc[M<<6],rc[M<<6],idx;
	inline void pushup(int x){s[x]=s[lc[x]]+s[rc[x]];}
	inline void pushdown(int x,int l,int r){
		if(tg[x]){
			if(!lc[x]) lc[x]=++idx;
			if(!rc[x]) rc[x]=++idx;
			int mid=(l+r)>>1;
			tg[lc[x]]+=tg[x],tg[rc[x]]+=tg[x];
			s[lc[x]]+=(mid-l+1)*tg[x],s[rc[x]]+=(r-mid)*tg[x];
			tg[x]=0;
		}
	}
	inline void chr(int &x,int a,int b,int v,int l,int r){
		if(!x) x=++idx;
		if(a<=l&&r<=b) return tg[x]+=v,s[x]+=(r-l+1)*v,void();
		int mid=(l+r)>>1;
		pushdown(x,l,r);
		if(a<=mid) chr(lc[x],a,b,v,l,mid);
		if(b>mid) chr(rc[x],a,b,v,mid+1,r);
		pushup(x);
	}
	inline int qry(int x,int a,int b,int l,int r){
		if(a<=l&&r<=b) return s[x];
		int mid=(l+r)>>1,z=0;
		pushdown(x,l,r);
		if(a<=mid) z+=qry(lc[x],a,b,l,mid);
		if(b>mid) z+=qry(rc[x],a,b,mid+1,r);
		return z;
	}
}tr;
//板子 1.5(使用标记永久化 1.52s)
struct SEG{
	int s[M<<5],tg[M<<5],lc[M<<5],rc[M<<5],idx;
	inline void chr(int &x,int a,int b,int v,int l,int r){
		if(!x) x=++idx;
		s[x]+=(b-a+1)*v;
		if(a<=l&&r<=b) return tg[x]+=v,void();
		int mid=(l+r)>>1;
		if(b<=mid) chr(lc[x],a,b,v,l,mid);
		else if(a>mid) chr(rc[x],a,b,v,mid+1,r);
		else chr(lc[x],a,mid,v,l,mid),chr(rc[x],mid+1,b,v,mid+1,r);
	}
	inline int qry(int x,int a,int b,int l,int r){
		if(a<=l&&r<=b) return s[x];
		int mid=(l+r)>>1,z=(b-a+1)*tg[x];
		if(b<=mid) return z+qry(lc[x],a,b,l,mid);
		if(a>mid) return z+qry(rc[x],a,b,mid+1,r);
		return z+qry(lc[x],a,mid,l,mid)+qry(rc[x],mid+1,b,mid+1,r);
	}
}tr;
//板子 2
struct SEG{
	int s[N<<2],t1[N<<2],t2[N<<2];
	inline void pushup(int x){s[x]=(s[lc]+s[rc])%P;}
	inline void pushdown(int x,int l,int r){
		if(t2[x]^1){
			(t2[lc]*=t2[x])%=P,(t2[rc]*=t2[x])%=P;
			(t1[lc]*=t2[x])%=P,(t1[rc]*=t2[x])%=P;
			(s[lc]*=t2[x])%=P,(s[rc]*=t2[x])%=P;
			t2[x]=1;
		}
		if(t1[x]){
			int mid=(l+r)>>1;
			(t1[lc]+=t1[x])%=P,(t1[rc]+=t1[x])%=P;
			(s[lc]+=t1[x]*(mid-l+1))%=P,(s[rc]+=t1[x]*(r-mid))%=P;
			t1[x]=0;
		}
	}//先乘后加。
	inline void build(int x,int l,int r){
		t1[x]=0,t2[x]=1;
		if(l==r) return s[x]=a[l]%P,void();
		int mid=(l+r)>>1;
		build(lc,l,mid),build(rc,mid+1,r);
		pushup(x);
	}
	inline void chr(int x,int a,int b,int v,int l,int r){
		if(a<=l&&r<=b) return (s[x]+=v*(r-l+1))%=P,(t1[x]+=v)%=P,void();
		int mid=(l+r)>>1;
		pushdown(x,l,r);
		if(a<=mid) chr(lc,a,b,v,l,mid);
		if(b>mid) chr(rc,a,b,v,mid+1,r);
		pushup(x);
	}
	inline void mur(int x,int a,int b,int v,int l,int r){
		if(a<=l&&r<=b) return (s[x]*=v)%=P,(t1[x]*=v)%=P,(t2[x]*=v)%=P,void();
		int mid=(l+r)>>1;
		pushdown(x,l,r);
		if(a<=mid) mur(lc,a,b,v,l,mid);
		if(b>mid) mur(rc,a,b,v,mid+1,r);
		pushup(x);
	}
	inline int qry(int x,int a,int b,int l,int r){
		if(a<=l&&r<=b) return s[x];
		int mid=(l+r)>>1,z=0;
		pushdown(x,l,r);
		if(a<=mid) z+=qry(lc,a,b,l,mid);
		if(b>mid) z+=qry(rc,a,b,mid+1,r);
		return z%P;
	}
}tr;

字典树

板子 最长异或路径(01-Trie)

多测注意 \(0\) 节点也要清空。

inline void clr(int x){memset(tr[x],0,sizeof tr[x]),cnt[x]=0;}
inline void ins(string& s){
	int p=0;
	for(char i:s){
		int c=getidx(i);
		if(!tr[p][c]) tr[p][c]=++idx;
		p=tr[p][c],cnt[p]++;
	}
}
inline int qry(string& s){
	int p=0;
	for(char i:s){
		int c=getidx(i);
		if(!tr[p][c]) return 0;
		p=tr[p][c];
	}
	return cnt[p];
}

算法

二分图

染色法判定

略。

匈牙利算法 最大匹配

bool dfs(int u){
	for(int i:G[u]){
		if(vis[i]) continue;
		vis[i]=1;
		if(!mch[i]||dfs(mch[i]))
			return mch[i]=u,1;
	}
	return 0;
}

欧拉图

  • 无向图是欧拉图\(\iff\)非零度节点连通,所有节点度数为偶。此时起点可以选任意节点。
  • 无向图是半欧拉图\(\iff\)非零度节点连通,恰有\(2\)个节点度数为奇。此时起点可以选两个奇度节点之一。
  • 有向图是欧拉图\(\iff\)非零度节点强连通,每个节点出入度相等。此时起点可以选任意节点。
  • 有向图是半欧拉图\(\iff\)非零度节点弱连通,至多一个顶点出度\(-\)入度\(=1\),至多一个顶点入度\(-\)出度\(=1\),其他节点出入度相等。此时起点是那个出度\(-\)入度\(=1\)的节点。

判定

输出

将要输出的内容(边/点)存入栈st中,输出时逐个弹栈即可;p初始全为\(0\)

由于要求按最小字典序输出,所以需要对出边从小到大排序,因此使用了邻接表存储。

输出途径点(有向图) - P7771 【模板】欧拉路径
void dfs(int u){
	for(int i=p[u];i<out[u];i=p[u]){
		p[u]++;
		dfs(G[u][i]);
	}
	st.push(u);
}
输出途径点(无向图) - P2731 [USACO3.3] 骑马修栅栏 Riding the Fences
void dfs(int u){
	for(int i=p[u];i<out[u];i=p[u]){
		p[u]++;
		if(cnt[u][v]) cnt[u][v]--,cnt[v][u]--,dfs(G[u][i]);
	}
	st.push(u);
}
输出途径边(有向图) - P1127 词链
void dfs(int u){
	for(int i=p[u];i<out[u];i=p[u]){
		p[u]++;
		dfs(G[u][i].to);
		st.push(G[u][i].id);
	}
}
输出途径边(无向图) - [暂无]
void dfs(int u){
	for(int i=p[u];i<out[u];i=p[u]){
		p[u]++;
		if(cnt[u][v]) cnt[u][v]--,cnt[v][u]--,dfs(G[u][i].to);
		st.push(G[u][i].id);
	}
}

拓扑排序

for(int i=1;i<=n;i++) if(!deg[i]) q.push(i);
while(!q.empty()){
	int t=q.front();
	q.pop();
	cout<<t<<" ";
	for(auto i:G[t]){
		deg[i]--;
		if(!deg[i]) q.push(i);
	}
}

Tarjan ~ Here

强连通分量

缩点

void tarjan(int x){
	low[x]=dfn[x]=++tim;
	st.push(x);
	vis[x]=1;
	for(auto i:G[x]){
		if(!dfn[i]){//没访问过
			tarjan(i);
			low[x]=min(low[x],low[i]);
		}else if(vis[i]){//访问过,且在栈中
			low[x]=min(low[x],dfn[i]);
		}
	}
	if(dfn[x]==low[x]){
		while(1){
			int t=st.top();
			st.pop();
			vis[t]=0;
			if(t==x) break;
		}
	}
}

割点

两种情况:

  • \(u\) 为根且子节点 \(\ge 2\) 个。
  • \(u\) 不为根且存在子节点 \(v\) 使得 \(low[v]\ge dfn[u]\)
void tarjan(int u){
	dfn[u]=low[u]=++tim;
	int ch=0;
	for(int i:G[u]){
		if(!dfn[i]){
			ch++;
			tarjan(i);
			low[u]=min(low[u],low[i]);
			if(u!=root&&low[i]>=dfn[u]) is[u]=1;
		}else low[u]=min(low[u],dfn[i]);
	}
	if(u==root&&ch>=2) is[u]=1;
}

割边

\(u\) 的子节点 \(v\) 使得 \(low[v]>dfn[u]\)

为了处理重边,请使用链式前向星存图,并记录过来边的编号。

void tarjan(int u,int from){
	dfn[u]=low[u]=++tim;
	for(int i=head[u]);i;i=e[i].nxt){
		int v=e[i].to;
		if((v^1)==from) continue;
		if(!dfn[v]){
			tarjan(v,i);
			low[u]=min(low[u],low[v]);
			if(low[v]>dfn[u]) cout<<i<<" 是割边\n";
		}else low[u]=min(low[u],dfn[v]);
	}
}

点双

两个性质:

  • 两个点双最多只有一个公共点,且这个点一定是割点。
  • 对于一个点双,它在 DFS 树中 dfn 值最小的点一定是割点 / 树根。

由此得到下面的过程。

void tarjan(int u,int from){
	dfn[u]=low[u]=++tim,st[++top]=u;
	int ch=0;
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(i==(from^1)) continue;
		if(!dfn[v]){
			ch++;
			tarjan(v,i);
			low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u]){
				vcnt++;
				do vdcc[vcnt].eb(st[top--]);
				while(st[top+1]!=v);
				vdcc[vcnt].eb(u);//割点/树根也需要加入点双
			}
		}else low[u]=min(low[u],dfn[v]);
	}
	if(u==root&&ch==0) vdcc[++vcnt].eb(u);
}

边双

只需在强连通分量的基础上,限制不走反边即可。

与割边类似地,为了处理重边,请使用链式前向星存图,并记录过来边的编号。

void tarjan(int u,int from){
	dfn[u]=low[u]=++tim,st[++top]=u;
	for(int i=head[u];i;i=e[i].nxt){
		if(i==(from^1)) continue;
		int v=e[i].to;
		if(!dfn[v]) tarjan(v,i),low[u]=min(low[u],low[v]);
		else low[u]=min(low[u],dfn[v]);
	}
	if(dfn[u]==low[u]){
		cnt++;
		while(1){
			int t=st[top--];
			eDCC[cnt].emplace_back(t);
			if(t==u) break;
		}
	}
}

最短路

一些较常见的变形

见这里

SPFA

inline bool spfa(){//1为存在负环
	d[1]=0,vis[1]=1,q.push(1);
	while(!q.empty()){
		int u=q.front();
		q.pop(),vis[u]=0;
		for(Ed i:G[u]){
			int v=i.to,w=i.w;
			if(d[u]+w<d[v]){
				d[v]=d[u]+w;
				cnt[v]=cnt[u]+1;//更新包含边数
				if(cnt[v]==n) return 1;
				if(!vis[v]) q.push(v),vis[v]=1;
			}
		}
	}
	return 0;
}

Dijkstra,Floyd

略。

扫描线

板子

struct SEG{
	int lp[N<<4],rp[N<<4],sum[N<<4],len[N<<4];
	void build(int x,int l,int r){
		lp[x]=l,rp[x]=r;
		if(l==r) return;
		int mid=(l+r)>>1;
		build(lc,l,mid),build(rc,mid+1,r);
	}
	void pushup(int x){
		if(sum[x]) len[x]=X[rp[x]+1]-X[lp[x]];
		else len[x]=(lp[x]!=rp[x])*(len[lc]+len[rc]);
	}
	void chr(int x,int a,int b,int v){
		if(X[rp[x]+1]<=a||b<=X[lp[x]]) return;
		if(a<=X[lp[x]]&&X[rp[x]+1]<=b) sum[x]+=v;
		else chr(lc,a,b,v),chr(rc,a,b,v);
		pushup(x);
	}
}seg;
struct Line{
	int l,r,h,o;
	bool operator < (const Line &_) const{return h<_.h;}
}s[N<<1];
signed main(){
	cin>>n;
	for(int i=1,x,y,xx,yy;i<=n;i++){
		cin>>x>>y>>xx>>yy;
		X[i]=x,X[i+n]=xx;
		s[i]={x,xx,y,1};
		s[i+n]={x,xx,yy,-1};
	}
	n<<=1;
	sort(s+1,s+1+n);
	sort(X+1,X+1+n);
	tn=unique(X+1,X+1+n)-X-1;
	seg.build(1,1,tn-1);
	for(int i=1;i<n;i++){
		seg.chr(1,s[i].l,s[i].r,s[i].o);
		ans+=seg.len[1]*(s[i+1].h-s[i].h);
	}
	cout<<ans<<"\n";
	return 0;
}

字符串哈希 ~ Here

inline ull f(ull d[],int l,int r){return d[r]-d[l-1]*pw[r-l+1];}
//...
for(int i=1;i<=n;i++) d[i]=d[i-1]*B+s[i];

KMP ~ Here and Here

板子

//1-indexed
for(int i=2,j=1;i<=m;i++){
	while(b[i]!=b[j]&&j>1) j=nxt[j-1]+1;
	if(b[i]==b[j]) nxt[i]=j++;
}
for(int i=1,j=1;i<=n;i++){
	while(a[i]!=b[j]&&j>1) j=nxt[j-1]+1;
	if(a[i]==b[j]){
		if(j==m) cout<<i-j+1<<"\n";
		j++;
	}
}

Manacher ~ Here

注意补充特殊字符,如 abc \(\to\) $#a#b#c#

void get_d(){
	d[1]=1;
	for(int i=2,l,r=1;i<=n;i++){
		if(i<=r) d[i]=min(d[r-i+l],r-i+1);
		while(s[i-d[i]]==s[i+d[i]]) d[i]++;
		if(i+d[i]-1>r) l=i-d[i]+1,r=i+d[i]-1;
	}
}

树上差分

略。

LCA

略。

DP

略。

数学

排列组合

\[\binom{n}{m}=\frac{n!}{m!(n-m)!}\\ \]

\[\binom{n}{m}=\binom{n-1}{m}+\binom{n-1}{m-1} \]

费马小定理

\[a^{-1}\equiv a^{p-2}\pmod p \]

仅限 \(p\) 为质数。

exGCD

\(ax+by=c\) 有解,当且仅当 \(\gcd(a,b)\mid c\)

int exgcd(int a,int b,int& x,int& y){
	int d;
	if(b==0) x=1,y=0,d=a;
	else d=exgcd(b,a%b,y,x),y-=a/b*x;
	return d;
}

CRT ~ Here

\[x=\sum\limits_{i=1}^n c_i\times k_i\times a_i \]

其中:

  • \(c_i=\dfrac{N}{b_i}\)

  • \(N=\prod b_i\)

  • \(k_i\equiv c_i^{-1}\pmod{b_i}\)

cin>>n;
for(int i=1;i<=n;i++){
	cin>>b[i]>>a[i];
	fac*=b[i];
}
for(int i=1;i<=n;i++){
	int c=fac/b[i],k,t;
	exgcd(c,b[i],k,t);
	(ans+=(__int128)c*k*a[i])%=fac;
}
cout<<(long long)((ans+fac)%fac)<<"\n";

exCRT ~ Here

略。

威尔逊定理

略。

二项式定理

\[(a+b)^n=\sum_{i=0}^n \binom{n}{i} x^i y^{n-i} \]

推论:

\[\sum_{i=0}^n \binom{n}{i}=2^n \]

多重集合排列

\[\frac{n!}{n_1!n_2!\dots n_k!} \]

posted @ 2025-10-31 23:45  Sinktank  阅读(539)  评论(0)    收藏  举报
★CLICK FOR MORE INFO★ TOP-BOTTOM-THEME
Enable/Disable Transition
Copyright © 2023 ~ 2025 Sinktank - 1328312655@qq.com
Illustration from 稲葉曇『リレイアウター/Relayouter/中继输出者』,by ぬくぬくにぎりめし.