ICPC-notes

动态规划

背包DP

分组背包

给定一个载荷为 c c c的背包和 m m m组物品,每组物品有$ a_i$个,每个物品都有它的价值和重量。每组物品里最多只能选一个,求怎样选能使背包总价值最大。

for(int i=0;i<m;i++){ // 遍历每一组
    for(int j=c-1;j>=a[i].min_elm();j--){ // 遍历DP区间,从大到小
        for(node x : a[i]){ // 遍历组里的每个元素
            dp[j]=max(dp[j], dp[j-x.weight] + x.value);
        }
    }
}

例题:2020 CCPC 威海站 L Clock Master

线性DP

最长上升子序列(LIS)

思路:建立一个类似单调栈的结构,该结构的元素数即为当前的LIS值。当遍历到新元素时分类讨论:

若新元素大于栈顶元素,则直接push进栈里

若新元素小于等于栈顶,则二分找到第一个比它大的元素,并替换之

ACWING 895. 最长上升子序列

struct BiStack{
	static const int maxn=1e6+5;
	int a[maxn];
	int size=0;
	int top(){ return a[size-1]; }
	void pop(){ size--; }
	void push(int x){ a[size++]=x; }
    /** 二分查找>=x的元素,返回值为指针 */
	int* bifind(int x){ return std::lower_bound(a,a+size,x); } 
    void insert(int x){
        if(size==0 || top()<x) push(x);
        else *(bifind(x))=x;
    }
}s;

int main(){
	int n;
	scanf("%d",&n);
	while(n--){
		int x;
		scanf("%d",&x);
        s.insert(x);
	}
	printf("%d",s.size);
	return 0;
}

洛谷 P1020 导弹拦截

对于最长不下降子序列,只要将s.top() < x替换为s.top()<=x,将lower_bound换成upper_bound即可

对于最长下降子序列,把s.top()<x换成>,把lower_bound的比较器换成greater<int>()

Dilworth定理

洛谷 P1020 导弹拦截
洛谷 P1233 木棍加工

最少可分割为多少个不上升子序列等于最长上升子序列长度

各元素不重复的最长公共子序列

洛谷 P1439 【模板】最长公共子序列

将字符串1各个元素的索引值记录到map中,用map值作为字符串2各个元素的“大小”来求一遍LIS

区间DP

一般情况下假设状态为 f ( l , l e n ) f(l,len) f(l,len) l l l为左端点, l e n len len为从 l l l开始的区间长度,取值为 [ 0 , n − l ) [0,n-l) [0,nl)。部分题目区分左右端点的情况,可以扩展成 f ( f l a g , l , l e n ) f(flag,l,len) f(flag,l,len)的形式。

初始值一般是设置 f ( i , 0 ) f(i,0) f(i,0)的值。一般按照len->left->mid的顺序嵌套遍历。

例题:P1063 能量项链 - 洛谷

	for(ll len=1;len<n;len++){
		for(ll l=0;l<2*n;l++){
			for(ll mid=1;mid<=len;mid++){
				f[l][len]=max(f[l][len],f[l][mid-1]+f[l+mid][len-mid]+wl[l]*wl[l+mid]*wr[l+len]);
			}
		}
	}

对于具有“时间”等额外参数的,可以考虑将额外参数的贡献在转移方程中计算出来。

例题:洛谷 P1220 关路灯

对于具有“类型”或“种类”参数的,如果种类较少,可以考虑压缩到每一位中。

例题:洛谷 P3146 [USACO16OPEN]248 G

实际上,上面这个题可以证明每次只考虑最大贡献的种类即可。

树DP

树形DP

给你一个 N N N个点的有根树,每条边都有一个权重。求保留 Q Q Q条边后各边权重之和的最大值。

相当于在树上用dfs求了个分组背包

for(int v:g[u]){ // 遍历每个子节点
	if(v==fa) continue;
	for(int i=min(child[u],Q); i>=0; i--){ // 遍历DP区间,即边数范围,从大到小
		int bound=min(child[v], i-1); // i-1是因为子节点到父节点自带一条边
		for(int k=0; k<=bound; k++){ // 遍历子节点的DP值
			dp[u][i]=max(dp[u][i], dp[u][i-1-k]+dp[v][k]+w[v]);
		}
	}
}

例题:洛谷 P2015 二叉苹果树

遍历有效子节点个数问题

2020 ICPC 南京站 M Monster Hunter (树形DP)

每个子节点都分有效和无效两种状态,不同状态时贡献不同,问你有效节点数为x时最大贡献是多少。

建立状态 f ( a l i v e , u , c n t ) f(alive,u,cnt) f(alive,u,cnt) a l i v e alive alive表示是否存活, c n t cnt cnt表示存活的子节点个数。转移方程分“子节点有/无效=》父节点有/无效”四种状态。初始状态分当前节点存活和不存活两种情况。

巧妙的设置转移方向和DP区间可以大幅降低时间复杂度,南京站这道题就考察了这点。

void dfs(int u){
	f[0][u][0]=0;
	f[1][u][1]=w[u];
	siz[u]=1;
	for(int v:g[u]){
		dfs(v);
		for(int j=siz[u];j>=0;j--){
			for(int k=siz[v];k>=0;k--){
				f[0][u][j+k]=min(f[0][u][j+k],f[0][u][j]+min(f[0][v][k],f[1][v][k]));
				f[1][u][j+k]=min(f[1][u][j+k],f[1][u][j]+min(f[0][v][k],f[1][v][k]+w[v]));
			}
		}
		siz[u]+=siz[v];
	}
}

换根DP

给你一棵 N N N个点的有根树,设 s u m [ x ] sum[x] sum[x] x x x到其他点距离之和,求最小的 s u m [ x ] sum[x] sum[x]

可以用up and down法做两遍DFS。用dp数组表示sum数组。先任选一个点为根节点,一遍dfs求出每个节点 x i x_i xi x i x_i xi所有子节点的距离之和存入dp数组里。

然后进行换根操作,对于一个节点 u u u来说,树上的节点可以分为两类:

  • 属于 u u u及其子树上的点:它们到 u u u的距离和即为 d p [ u ] dp[u] dp[u]

  • 其他的点:它们的距离和 = = = u u u父节点的距离和 + + +这些节点个数 ∗ d i s ( u , f a ) *dis(u,fa) dis(u,fa)

按照前序dfs遍历时可推出如下公式:

d p [ u ] = d p [ u ] + ( d p [ f a ] − d p [ u ] ) − ( c h i l d [ u ] + 1 ) ∗ d i s ( u , f a ) + ( N − c h i l d [ u ] − 1 ) ∗ d i s ( u , f a ) dp[u]=dp[u] + (dp[fa]-dp[u])-(child[u]+1)*dis(u,fa)+(N-child[u]-1)*dis(u,fa) dp[u]=dp[u]+(dp[fa]dp[u])(child[u]+1)dis(u,fa)+(Nchild[u]1)dis(u,fa)

( d p [ f a ] − d p [ u ] ) − ( c h i l d [ u ] + 1 ) ∗ d i s ( u , f a ) (dp[fa]-dp[u])-(child[u]+1)*dis(u,fa) (dp[fa]dp[u])(child[u]+1)dis(u,fa)表示从 d p [ f a ] dp[fa] dp[fa]里扣掉 u u u及其子树和它们在 ( u , f a ) (u,fa) (u,fa)这条路径上的贡献, ( N − c h i l d [ u ] − 1 ) ∗ d i s ( u , f a ) (N-child[u]-1)*dis(u,fa) (Nchild[u]1)dis(u,fa)表示除 u u u及其子树之外的点经过 ( u , f a ) (u,fa) (u,fa)时的贡献。

化简得: d p [ u ] = d p [ f a ] + ( N − 2 ∗ c h i l d [ u ] − 2 ) ∗ d i s ( u , f a ) dp[u]=dp[fa]+(N-2*child[u]-2)*dis(u,fa) dp[u]=dp[fa]+(N2child[u]2)dis(u,fa)

例题:2020 CCPC 威海站 C Rencontre

树上哈希

题目大意:给你 M M M棵树,判断它们是否能变成相同的形态。树棵数不超过50,树上节点不超过50。

处理无根树同构的通用方法是选定一个根来将整个树映射成一个32位或64位Hash值,然后比较以重心为根的Hash值(一棵树最多俩重心)。如果树棵数少的话也可以粗暴的比较每棵树所有节点的Hash值。

以常见的素数哈希法为例:设 f x f_x fx表示以 x x x为根的子树的Hash值, s o n x son_x sonx表示 x x x的子节点集合, s i z e y size_y sizey表示以 y y y为根的子树的规模(即子节点总数+1,记住要+1), p r i m e ( i ) prime(i) prime(i)表示素数表里的第 i i i项。则有:

f x = 1 + ∑ y ∈ s o n x f y × p r i m e ( s i z e y ) f_x=1+\sum_{y \in son_x}f_y \times prime(size_y) fx=1+ysonxfy×prime(sizey)

在实现的时候可以运用up and down树形DP的思想,先任选一个点 x x x为根,第一遍DFS求出重心和以 x x x为根的树的hash数组,第二遍DFS求出每个点为根时的hash数组。

素数表可以运用各种素数筛制作,最好对生成的素数表过遍random_shuffle,防止部分出题人故意卡素数哈希。

常见操作:

dp[u]+=dp[v]*p[siz[v]]; //第一遍DFS,访问子节点后调用
dp[v]+=(dp[u]-dp[v]*p[siz[v]])*(p[N-siz[v]]); //第二遍DFS,访问子节点前调用

例题:洛谷 P5043 树同构 (换根DP+树上哈希)

数位DP

typedef long long ll;
int a[20]; //按位分解后的数
ll dp[20][state];//不同题目状态不同
ll dfs(int pos,/*state变量*/,bool lead/*前导零*/,bool `/*数位上界变量*/)//不是每个题都要判断前导零
{
	//递归边界,既然是按位枚举,最低位是0,那么pos==-1说明这个数我枚举完了
	if(pos==-1) return 1;/*这里一般返回1,表示你枚举的这个数是合法的,那么这里就需要你在枚举时必须每一位都要满足题目条件,也就是说当前枚举到pos位,一定要保证前面已经枚举的数位是合法的。不过具体题目不同或者写法不同的话不一定要返回1 */
	//第二个就是记忆化(在此前可能不同题目还能有一些剪枝)
	if(!limit && !lead && dp[pos][state]!=-1) return dp[pos][state];
	/*常规写法都是在没有限制的条件记忆化,这里与下面记录状态是对应,具体为什么是有条件的记忆化后面会讲*/
	int up=limit?a[pos]:9;//根据limit判断枚举的上界up;这个的例子前面用213讲过了
	ll ans=0;
	//开始计数
	for(int i=0;i<=up;i++)//枚举,然后把不同情况的个数加到ans就可以了
	{
		if() ... // 一般是continue掉无效状态
		else if()...
		ans+=dfs(pos-1,/*状态转移*/,lead && i==0,limit && i==a[pos]) //最后两个变量传参都是这样写的
		/*这里还算比较灵活,不过做几个题就觉得这里也是套路了
		大概就是说,我当前数位枚举的数是i,然后根据题目的约束条件分类讨论
		去计算不同情况下的个数,还有要根据state变量来保证i的合法性,比如题目
		要求数位上不能有62连续出现,那么就是state就是要保存前一位pre,然后分类,
		前一位如果是6那么这意味就不能是2,这里一定要保存枚举的这个数是合法*/
	}
	//计算完,记录状态
	if(!limit && !lead) dp[pos][state]=ans;
	/*这里对应上面的记忆化,在一定条件下时记录,保证一致性,当然如果约束条件不需要考虑lead,这里就是lead就完全不用考虑了*/
	return ans;
}
ll solve(ll x)
{
	int pos=0;
	while(x)//把数位都分解出来
	{
		a[pos++]=x%10;//个人老是喜欢编号为[0,pos),看不惯的就按自己习惯来,反正注意数位边界就行
		x/=10;
	}
	return dfs(pos-1/*从最高位开始枚举*/,/*一系列状态 */,true,true);//刚开始最高位都是有限制并且有前导零的,显然比最高位还要高的一位视为0嘛
}
int main()
{
	ll le,ri;
	while(~scanf("%lld%lld",&le,&ri))
	{
		//初始化dp数组为-1,这里还有更加优美的优化,后面讲
		printf("%lld\n",solve(ri)-solve(le-1));
	}
}

图论

树论

无根树转有根树

DFS递归

生成树

生成树(Spanning Tree):给定一个无向图,如果它的某个子图中任意两个定点都相互连通,并且构成一棵树

最小生成树(MST,Minimum Spanning Tree):边权之和最小的生成树
Kruskal算法

const int MAXN=1e6+5;
int u[MAXN],v[MAXN],w[MAXN],r[MAXN],p[MAXN];
int cmp(const int i,const int j){ return w[i]<w[j];}
int findpa(int x){return p[x]==x?x:p[x]=findpa(p[x]);}
int Kruskal(int n,int m){
	int ans=0;
	for(int i=0;i<n;i++) p[i]=i;
	for(int i=0;i<m;i++) r[i]=i;
	sort(r,r+m,cmp);
	for(int i=0;i<m;i++){
		int e=r[i];
		int x=findpa(u[e]);
		int y=findpa(v[e]);
		if(x!=y){
			ans+=w[e];
			p[x]=y;
		}
	}
	return ans;
}
int main(){
	ios::sync_with_stdio(0);
	int n,m;
	cin>>n>>m;
	for(int i=0;i<m;i++){
		int x,y,z;
		cin>>x>>y>>z;
		x--;
		y--;
		u[i]=x;
		v[i]=y;
		w[i]=z;//边的方向堆答案无影响
	}
	int ans=Kruskal(n,m);
	for(int i=1;i<n;i++){
		if(findpa(i)!=findpa(0)){
			cout<<"disconnected";
			return 0;
		}
	}
	cout<<ans;
	return 0;
}

LCA

求树上两点的最近公共最先。

倍增法实现:

#include<cstdio>
#include<iostream>
using namespace std;
int n,m,s,num=0,head[1000001],dep[1000001],f[1000001][23];
// dep:深度 f:倍增的祖先 两节点距离=dep[a]+dep[b]-2*dep[lca(a,b)]
struct edg{
    int next,to;
}edge[1000001];
void edge_add(int u,int v)//链式前向星存图 
{
    num++;
    edge[num].next=head[u];edge[num].to=v;head[u]=num;
    edge[++num].next=head[v];edge[num].to=u;head[v]=num;
}
void dfs(int u,int father)//对应深搜预处理f数组 
{
    dep[u]=dep[father]+1;
    for(int i=1;(1<<i)<=dep[u];i++)
    {
        f[u][i]=f[f[u][i-1]][i-1];
    }
    for(int i=head[u];i;i=edge[i].next)
    {
        int v=edge[i].to;
        if(v==father)continue;//双向图需要判断是不是父亲节点 
        f[v][0]=u;
        dfs(v,u);
    }
}
int lca(int x,int y)
{
    if(dep[x]<dep[y])swap(x,y);
    for(int i=20;i>=0;i--)//从大到小枚举使x和y到了同一层 
    {
        if(dep[f[x][i]]>=dep[y])x=f[x][i];
        if(x==y)return x;
    }
    for(int i=20;i>=0;i--)//从大到小枚举 
    {
        if(f[x][i]!=f[y][i])//尽可能接近 
        {
            x=f[x][i];y=f[y][i];
        } 
    } 
    return f[x][0];//随便找一个**输出 
}
int main(){
    scanf("%d%d%d",&n,&m,&s);
    for(int i=1;i<n;i++)
    {
        int a1,a2;
        scanf("%d",&a1);scanf("%d",&a2);
        edge_add(a1,a2);//链式存边 
    }
    dfs(s,0);
    for(int i=1;i<=m;i++)
    {
        scanf("%d %d",&a1,&a2);
        printf("%d\n",lca(a1,a2));//求两个节点的LCA 
    }
}

图论

无向图

连通图:任意两点有路径连通的图

度:节点连接的边数

树:无环的连通图

森林:无环的非连通图

有向图

DAG:Directed Acyclic Graph,无环有向图

拓扑序:对每一个节点编号,使得若存在节点i到节点j的边,则保证编号i < j

判环:若能求出拓扑序,则不含环

求环上的点:两遍dfs,第一次dfs标记遇到的深度小于当前点且已被遍历的点,第二次对第一次dfs标记点的后代节点染色

着色

着色:把相邻点染成不同颜色,常用DFS解决

最小着色:对图染色所需要的最小颜色数

二分图:最小着色为2的图

最短路

Dijkstra
const int inf=0x3f3f3f3f;
const int maxn=100+10;
struct  qnode
{
	int v,c;
	qnode(int vv=0,int cc=0):v(vv),c(cc){}
	bool operator <(const qnode &r)const{
		return c>r.c;
	}
};
struct Edge
{
	int v,c;
	Edge(int vv=0,int cc=0):v(vv),c(cc){}
};
vector<Edge> e[maxn];
bool vis[maxn];
int dist[maxn];
void dijkstra(int n,int start){
	memset(vis,false,sizeof(vis));
	for(int i=0;i<=n;i++) dist[i]=inf;
	dist[start]=0;

	priority_queue<qnode> q;
	q.push(qnode(start,0));
	qnode t;
	while(!q.empty()){
		t=q.top();
		q.pop();
		int u=t.v;
		if(vis[u]) continue;
		vis[u]=true;
		for(int i=0;i<e[u].size();i++){
			int v=e[u][i].v;
			int c=e[u][i].c;
			if(!vis[v]&&dist[v]>dist[u]+c){
				dist[v]=dist[u]+c;
				q.push(qnode(v,dist[v]));
			}
		}
	}
}
void addedge(int u,int v,int w){
	e[u].push_back(Edge(v,w));
}
Bellman-Ford

负权最短路

const int maxn=100+10;
const int maxm=10000+10;
const int inf=0x3f3f3f3f;
struct Edge
{
	int v,c;
	Edge(int vv,int cc):v(vv),c(cc){}
};
vector<Edge> e[maxn];
bool vis[maxn];
int cnt[maxn];
int dist[maxn];
bool spfa(int n,int start){
	memset(vis,false,sizeof(vis));
	vis[start]=true;
	memset(dist,inf,sizeof(dist));
	dist[start]=0;
	memset(cnt,0,sizeof(cnt));
	cnt[start]=1;
	queue<int> q;
	q.push(start);
	while(!q.empty()){
		int u=q.front();
		q.pop();
		vis[u]=false;
		for(int i=0;i<e[u].size();i++){
			int v=e[u][i].v;
			if(dist[v]>dist[u]+e[u][i].c){
				dist[v]=dist[u]+e[u][i].c;
				if(!vis[v]){
					vis[v]=true;
					q.push(v);
					cnt[v]++;
					if(cnt[v]>n) return false;
				}
			}
		}
	}
	return true;
}
void addedge(int u,int v,int w){   
	e[u].push_back(Edge(v,w));
}

查分约束系统:由形如Xj-Xi<=Bk的不等式组成的不等式组。类似于d[v]<=d[u]+w(u,v)。对于每个条件Xj-Xi<=Bk,从节点i到j连一条权值为Bk的边,再加一个源点向每个点连一条权值为0的边,跑一遍BF。若失败则无解,若成功则源点到每个点的距离即为xi的值。同理,求x[n-1] - x[0] 的最大值就是求0到n-1的最短路。

Floyd-Warshall
for(int k=0;k<n;k++){
    for(int i=0;i<n;i++){
        for(int j=0;j<n;j++){
            if(g[i][j]>g[i][k]+g[k][j]){
                g[i][j]=g[i][k]+g[k][j];
            }
        }
    }
}

最大流

一个有向图,每条边都有个容量,指定起点终点,求最大流量
Dinci

const int maxn = 10000 + 10;
const int inf = 0x3f3f3f3f;
struct Edge
{
	int from, to, cap, flow;
};
struct Dinic
{
	int m = 0, s = 0, t = 0;
	vector<Edge> edges;
	vector<int> g[maxn];
	bool vis[maxn];
	int d[maxn];
	int cur[maxn];
	bool bfs()
	{
		memset(vis, 0, sizeof(vis));
		queue<int> q;
		q.push(s);
		d[s] = 0;
		vis[s] = 1;
		while (!q.empty())
		{
			int x = q.front();
			q.pop();
			for (int i = 0; i < g[x].size(); i++)
			{
				Edge &e = edges[g[x][i]];
				if (!vis[e.to] && e.cap > e.flow)
				{
					vis[e.to] = 1;
					d[e.to] = d[x] + 1;
					q.push(e.to);
				}
			}
		}
		return vis[t];
	}
	int dfs(int x, int a)
	{
		if (x == t | a == 0)
			return a;
		int maxflow = 0, f;
		for (int &i = cur[x]; i < g[x].size(); i++)
		{
			Edge &e = edges[g[x][i]];
			if (d[x] + 1 == d[e.to] && (f = dfs(e.to, min(a, e.cap - e.flow))) > 0)
			{
				e.flow += f;
				edges[g[x][i] ^ 1].flow -= f;
				maxflow += f;
				a -= f;
				if (a == 0)
					break;
			}
		}
		return maxflow;
	}
	int Maxflow(int start, int end)
	{
		s = start;
		t = end;
		int flow = 0;
		while (bfs())
		{
			memset(cur, 0, sizeof(cur));
			flow += dfs(s, inf);
		}
		return flow;
	}
	void clear(int n)
	{
		edges.clear();
		for (int i = 0; i < n; i++)
			g[i].clear();
	}
	void addedge(int from, int to, int cap)
	{
		edges.push_back((Edge){from, to, cap, 0});
		edges.push_back((Edge){to, from, 0, 0});
		m = edges.size();
		g[from].push_back(m - 2);
		g[to].push_back(m - 1);
	}
};
Dinic dn;

最小费用最大流

在最大流基础上每条边多了个单位费用属性,求总费用最小的最大流
下为能求浮点数的版本。求整数时把double换成int,把eps设成0,并修改注释处
2016 ACM/ICPC 青岛 G Coding Contest (浮点数网络流+取对数)

#include <bits/stdc++.h>
using namespace std;
const int INF=0x3f3f3f3f;
const int maxv=1e5+10; //最大节点数
const double eps=1e-8;

double mcost;
struct edge{
	int to,cap,rev;
	double cost;
};
vector<edge> G[maxv];
bool vis[maxv];
double dist[maxv];
void init(){
	mcost=0;
	for(int i=0;i<maxv;i++) G[i].clear();
}

void add_edge(int from, int to, int cap, double cost){
	G[from].push_back(edge{to,cap,(int)G[to].size(),cost});
	G[to].push_back(edge{from,0,(int)G[from].size()-1,-cost});
}

bool spfa(int s,int t){
	memset(vis,false,sizeof(vis));
	for(int i=0;i<maxv;i++) dist[i]=INF;
	dist[s]=0;
	queue<int> q;
	q.push(s); vis[s]=true;
	while(!q.empty()){
		int u=q.front(); q.pop();
		for(auto &e : G[u]){
			if(e.cap>0 && dist[e.to]>dist[u]+e.cost+eps){
				dist[e.to]=dist[u]+e.cost;
				if(!vis[e.to]){
					vis[e.to]=true;
					q.push(e.to);
				}
			}
		}
		vis[u]=false;
	}
	return dist[t]<INF-eps;
}

int dfs(int v,int t,int f){
	vis[v]=true;
	if(v==t) return f;
	int ret=0;
	for(auto &e:G[v]){
		if(!vis[e.to] && e.cap>0 && fabs(dist[v]+e.cost-dist[e.to])<eps){
			//费用为整数时替换为 abs(dist[v]+e.cost-dist[e.to])<=0
			int d=dfs(e.to, t, min(e.cap, f-ret));
			if(d){
				mcost+=e.cost*d;
				e.cap-=d;
				G[e.to][e.rev].cap+=d;
				ret+=d;
				if(ret==f) break;
			}
		}
	}
	return ret;
}

int costflow(int s,int t){
	int flow=0;
	while(spfa(s,t)){
		vis[t]=true;
		while(vis[t]){
			memset(vis,false,sizeof(vis));
			flow+=dfs(s,t,INF);
		}
	}
	return flow;
}

const int SUPER_SRC=105;
const int SUPER_DST=SUPER_SRC+1;
void solve(){
	init();// 初始化很重要!!!
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		int s,b;
		cin>>s>>b;
		add_edge(SUPER_SRC,i,s,0);
		add_edge(i,SUPER_DST,b,0);
	}
	while(m--){
		int u,v,cap;
		double w;
		cin>>u>>v>>cap>>w;
		w=-log(1.0-w);
		add_edge(u,v,cap-1,w);
		add_edge(u,v,1,0);
	}
	costflow(SUPER_SRC,SUPER_DST);
	printf("%.2lf\n",1.0-pow(exp(1.0),-mcost));
}

int main(){
	ios::sync_with_stdio(0);
	int T;
	cin>>T;
	while(T--){
		solve();
	}
	return 0;
}

割点

一个无向图中,如果删除某个顶点,这个图就不再连通,那么这个顶点就是这个图的割点,又被称为割顶

若删除某个边后,图不连通,则该边被称为割边(桥)

//洛谷P3388 割点
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn=20000+10;
vector<int> g[maxn];
int pre[maxn];
bool iscut[maxn];
int low[maxn];
int dfs_clock;
int dfs(int u,int fa){
	int lowu=pre[u]=++dfs_clock;
	int child=0;
	for(int i=0;i<g[u].size();i++){
		int v=g[u][i];
		if(!pre[v]){
			child++;
			int lowv=dfs(v,u);
			lowu=min(lowu,lowv);
			if(lowv>=pre[u]) iscut[u]=true;
		}
		else if(pre[v]<pre[u]&&v!=fa){
			lowu=min(lowu,pre[v]);
		}
	}
	if(fa<0&&child==1) iscut[u]=false;
	low[u]=lowu;
	return lowu;
}
int main(){
	memset(pre,0,sizeof(pre));
	memset(iscut,false,sizeof(iscut));
	memset(low,0,sizeof(low));
	dfs_clock=0;
	int n,m,a,b;
	cin>>n>>m;
	for(int i=0;i<m;i++){
		cin>>a>>b;
		g[a].push_back(b);
		g[b].push_back(a);
	}
	for(int i=1;i<=n;i++){
		if(!pre[i]) dfs(i,-1);
	}
	int ans=0;
	for(int i=1;i<=n;i++)
	{
		if(iscut[i]) ans++;
	}
	cout<<ans<<'\n';
	for(int i=1;i<=n;i++){
		if(iscut[i]) cout<<i<<' ';
	}
	return 0;
}

双连通分量BCC

若一个无向图的点两两间都有两条不相交(经过的点不一样)的路径,那么我们就称这个无向图是点-双连通的。条件等价于任意两条边都在一个简单环内。

类似的,若一个无向图的点两两间都有两条不重合(这个要求低一点,点可以重复,但边不行)的路径,那么我们就称这个无向图是边-双连通的。

//LA3523 / poj2942 双连通
#include <cstdio>
#include <stdio.h>
#include <algorithm>
#include <vector>
#include <cstring>
#include <stack>
using namespace std;
const int maxn=1000+10;


const int BLACK=-1;
const int WHITE=1;
const int NONE=0;
int color[maxn];
int noedge[maxn][maxn];
bool isodd[maxn];

struct Edge{
	int u,v;
};
int pre[maxn];
bool iscut[maxn];
int bccno[maxn];
int dfs_clock;
int bcc_cnt;
vector<int> g[maxn];
vector<int> bcc[maxn];
stack<Edge> s;
int dfs(int u,int fa){
	int lowu=pre[u]=++dfs_clock;
	int child=0;
	for(int i=0;i<g[u].size();i++){
		int v=g[u][i];
		Edge e=(Edge){u,v};
		if(!pre[v]){
			s.push(e);
			child++;
			int lowv=dfs(v,u);
			lowu=min(lowu,lowv);
			if(lowv>=pre[u]){
				iscut[u]=true;
				bcc_cnt++;
				bcc[bcc_cnt].clear();
				while(true){
					Edge x=s.top();
					s.pop();
					if(bccno[x.u]!=bcc_cnt){
						bcc[bcc_cnt].push_back(x.u);
						bccno[x.u]=bcc_cnt;
					}
					if(bccno[x.v]!=bcc_cnt){
						bcc[bcc_cnt].push_back(x.v);
						bccno[x.v]=bcc_cnt;
					}
					if(x.u==u&&x.v==v) break;
				}
			}
		}
		else if(pre[v]<pre[u]&&v!=fa){
			s.push(e);
			lowu=min(lowu,pre[v]);
		}
	}
	if(fa<0&&child==1) iscut[u]=false;
	return lowu;
}
void find_bcc(int n){
	memset(pre,0,sizeof(pre));
	memset(iscut,false,sizeof(iscut));
	memset(bccno,0,sizeof(bccno));
	dfs_clock=bcc_cnt=0;
	for(int i=0;i<n;i++){
		if(!pre[i]) dfs(i,-1);
	}
}

bool bipartite(int u,int bccid){
	for(int i=0;i<g[u].size();i++){
		int v=g[u][i];
		if(bccno[v]!=bccid) continue;
		if(color[v]==color[u]) return false;
		if(color[v]==0){
			color[v]=color[u]*(-1);
			if(!bipartite(v,bccid)) return false;
		}
	}
	return true;
}

int main(){
	int n,m,a,b;
	while(~scanf("%d%d",&n,&m)&&(n||m)){
		memset(noedge,0,sizeof(noedge));
		for(int i=0;i<n;i++){
			g[i].clear();
			bcc[i].clear();
		}
		while(!s.empty()) s.pop();
		for(int i=0;i<m;i++){
			scanf("%d%d",&a,&b);
			a--;
			b--;
			noedge[a][b]=1;
			noedge[b][a]=1;
		}
		for(int i=0;i<n;i++)
		for(int j=0;j<n;j++)
		if(i!=j&&(!noedge[i][j])){ //注意不要把自己和自己连一条边
			g[i].push_back(j);
		}
		find_bcc(n);
		memset(isodd,false,sizeof(isodd));
		for(int i=1;i<=bcc_cnt;i++){
			memset(color,NONE,sizeof(color));
			for(int j=0;j<bcc[i].size();j++) bccno[bcc[i][j]]=i; 
			//因为割顶可以是不同双连通分量的公共点,所以要将割顶编号与双联通分量统一
			int u=bcc[i][0]; //双连通分量的第一个几点用于启动染色
			color[u]=BLACK; //预染色
			if(!bipartite(u,i)){
				for(int j=0;j<bcc[i].size();j++) isodd[bcc[i][j]]=true;
			}
		}
		int ans=n;
		for(int i=0;i<n;i++) if(isodd[i]) ans--;
		printf("%d\n",ans);
	}
	return 0;
}

强连通分量SCC

在有向图G中,如果两个顶点u,v间有一条从u到v的有向路径,同时还有一条从v到u的有向路径,则称两个顶点强连通。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向非强连通图的极大强连通子图,称为强连通分量。

//P1726 强连通
#include <vector>
#include <cstdio>
#include <cstring>
#include <stack>
#include <algorithm>
using namespace std;
const int maxn=5000+10;
int pre[maxn],lowlink[maxn],sccno[maxn],dfs_clock,scc_cnt;//scc编号从0开始
stack<int> s;
vector<int> g[maxn];
vector<int> scc[maxn];
void dfs(int u){
	pre[u]=lowlink[u]=++dfs_clock;
	s.push(u);
	for(int i=0;i<g[u].size();i++){
		int v=g[u][i];
		if(!pre[v]){
			dfs(v);
			lowlink[u]=min(lowlink[u],lowlink[v]);
		}else if(!sccno[v]){
			lowlink[u]=min(lowlink[u],pre[v]);
		}
	}
	if(lowlink[u]==pre[u]){
		scc_cnt++;
		for(;;){
			int x=s.top();
			s.pop();
			sccno[x]=scc_cnt;
			if(x==u) break;
		}
	}
}
void find_scc(int n){
	dfs_clock=scc_cnt=0;
	memset(sccno,0,sizeof(sccno));
	memset(pre,0,sizeof(pre));
	for(int i=0;i<n;i++){
		if(!pre[i]) dfs(i);
	}
}
int main(){
	int n,m,a,b,x;
	scanf("%d%d",&n,&m);
	for(int i=0;i<n;i++){//节点编号从0开始
		g[i].clear();
		scc[i].clear();
	}
	for(int i=0;i<m;i++){
		scanf("%d%d%d",&a,&b,&x);
		a--;
		b--;
		if(x==1){
			g[a].push_back(b);
		}
		else if(x==2){
			g[a].push_back(b);
			g[b].push_back(a);
		}
	}
	find_scc(n);
	for(int i=0;i<n;i++){
		scc[sccno[i]].push_back(i);
	}
	int maxcnt=0;
	int maxid=0;
	for(int i=1;i<=scc_cnt;i++){
		if(scc[i].size()>maxcnt){
			maxcnt=scc[i].size();
			maxid=i;
		}
	}
	printf("%d\n",maxcnt);
	for(int i=0;i<scc[maxid].size();i++){
		if(i!=0) printf(" ");
		printf("%d",scc[maxid][i]+1);
	}
	return 0;
}

2SAT

对于bool变量 a 1 , a 2 , a 3 . . . . . a_1,a_2,a_3..... a1,a2,a3.....,求在满足形如“若 a i a_i ai v a l i val_i vali a j a_j aj v a l j val_j valj”的若干条约束同时成立时, a 1 , a 2 . . . . a_1,a_2.... a1,a2....的取值。

实际上就是一个有若干 a i [ v a l i ] − > a j [ v a l j ] a_i[val_i]->a_j[val_j] ai[vali]>aj[valj]组成的图。

对于KaTeX parse error: Undefined control sequence: \or at position 8: x=xval \̲o̲r̲ ̲y=yval式约束的问题,可以拆分成 x [ x v a l ] − > y [ ! y v a l ] x[xval]->y[!yval] x[xval]>y[!yval] y [ y v a l ] − > x [ x v a l ] y[yval]->x[xval] y[yval]>x[xval]两条边。

例题:P4782【模板】2-SAT 问题

对于x与y中至多一个为真的问题,可连边x->y^1y->x^1

例题:Codeforces 782 D. Innokenty and a Football League (2SAT)

#include <bits/stdc++.h>
using namespace std;
const int MAXN=2e6+5;
struct TwoSAT{
	int n;
	vector<int> G[MAXN*2];
	bool mark[MAXN*2];//mark中为每个点最终状态,且该解的字典序最小
	int S[MAXN*2],c;
	
	bool dfs(int x){
		if(mark[x^1]) return false;
		if(mark[x]) return true;
		mark[x]=true;
		S[c++]=x;
		for(int i=0;i<G[x].size();i++){
			if(!dfs(G[x][i])) return false;
		}
		return true;
	}

    // 切记初始化
	void init(int n){
		this->n=n;
		for(int i=0;i<n*2;i++) G[i].clear();
		memset(mark,0,sizeof(mark));
	}

	// x=xval or y=yval
	void add_clause(int x,int xval, int y,int yval){
		x=x*2+xval;
		y=y*2+yval;
		G[x^1].push_back(y);
		G[y^1].push_back(x);
	}

	bool solve(){
		for(int i=0;i<n*2;i+=2){
			if(!mark[i] && !mark[i+1]){
				c=0;
				if(!dfs(i)){
					while(c>0) mark[S[--c]]=false;
					if(!dfs(i+1)) return false;
				}
			}
		}
		return true;
	}
}ts;

int main(){
	ios::sync_with_stdio(0);
	int n,m;
	cin>>n>>m;
	ts.init(n);
	while(m--){
		int x,xval,y,yval;
		cin>>x>>xval>>y>>yval;
		ts.add_clause(x,xval,y,yval);
	}
	if(!ts.solve()){
		cout<<"IMPOSSIBLE";
	}
	else{
		cout<<"POSSIBLE\n";
		for(int i=3;i<=(n+1)*2;i+=2){
			cout<<ts.mark[i]<<" ";
		}
	}
	return 0;
}

数据结构

线段树

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

struct SetmentTree{
	/*
	下标从1开始
	MAXLEN=数组上限*4
	*/
	static const int MAXLEN=200000+10;
	ll a[MAXLEN];
	ll sum[MAXLEN*4];
	ll lazy[MAXLEN*4];
	int n;
	void init(int an){
		n=an;
		memset(lazy,0,sizeof(lazy));
	}
	void down(int ln,int rn,int k){ //懒惰标记下推
		lazy[k*2]+=lazy[k];
		lazy[k*2+1]+=lazy[k];
		sum[k*2]+=lazy[k]*ln;
		sum[k*2+1]+=lazy[k]*rn;
		lazy[k]=0;
	}
	void build(int l,int r,int k){ //构造线段树
		if(l==r){
			sum[k]=a[l];
			return;
		}
		int m=(l+r)/2;
		build(l,m,k*2);
		build(m+1,r,k*2+1);
		sum[k]=sum[k*2]+sum[k*2+1];
		return;
	}
	void update(int l,int r,int k,int aiml,int aimr,int value){ //区间修改
		if(aiml<=l&&aimr>=r){
			sum[k]+=value*(r-l+1);
			lazy[k]+=value;
			return;
		}
		int m=(l+r)/2;
		down(m-l+1,r-m,k);
		if(aiml<=m) update(l,m,k*2,aiml,aimr,value);
		if(aimr>m) update(m+1,r,k*2+1,aiml,aimr,value);
		sum[k]=sum[k*2]+sum[k*2+1];
	}
	void query(int l,int r,int k,int aiml,int aimr,ll& ans){ //区间查询
		if(l>=aiml&&r<=aimr){
			ans+=sum[k];
			return;
		}
		int m=(r+l)/2;
		down(m-l+1,r-m,k);
		if(aiml<=m) query(l,m,k*2,aiml,aimr,ans);
		if(aimr>m) query(m+1,r,k*2+1,aiml,aimr,ans); 
	}
	void reset(int l,int r,int k,int index,ll value){ //单点赋值
		if(l==r){
			sum[k]=value;
			return;
		}
		int m=(l+r)/2;
		down(m-l+1,r-m,k);
		if(index<=m) reset(l,m,k*2,index,value);
		else reset(m+1,r,k*2+1,index,value);
		sum[k]=sum[k*2]+sum[k*2+1];
	}
	void add(int l,int r,int k,int index,ll value){ //单点修改
		if(l==r){
			sum[k]+=value;
			return;
		}
		int m=(l+r)/2;
		down(m-l+1,r-m,k);
		if(index<=m) add(l,m,k*2,index,value);
		else add(m+1,r,k*2+1,index,value);
		sum[k]=sum[k*2]+sum[k*2+1];
	}
	ll ask(int l,int r,int k,int index){ //单点查询
		if(l==r){
			return sum[k];
		}
		int m=(l+r)/2;
		down(m-l+1,r-m,k);
		if(index<=m) return ask(l,m,k*2,index);
		else return ask(m+1,r,k*2+1,index);
	}
}st;
int main(){
	ios::sync_with_stdio(0);
	int n,m;
	ll x,y,z;
	cin>>n>>m;
	st.init(n);
	for(int i=1;i<=n;i++) cin>>st.a[i];
	st.build(1,n,1);
	while(m--){
		cin>>x;
		if(x==1){
			cin>>x>>y>>z;
			st.update(1,n,1,x,y,z);
		}
		else{
			cin>>x>>y;
			ll ans=0;
			st.query(1,n,1,x,y,ans);
			cout<<ans<<"\n";
		}
	}
	return 0;
}

单调队列

用来解决求滑动窗口中的每一节上的最大值/最小值

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

下面以单调上升队列为例

struct node{
    int value;
    int id;
};

deque<node> minq;

// 在保证单调递增前提下push新元素
// 单调递减队列只要把<=改为>=
void push_min(int x,int id){
    while(!minq.empty() && x<=minq.back().value){
        minq.pop_back();
    }
    minq.push_back(node{x,id});
}

// 弹出索引值小于id的元素
// 要么每次迭代新索引时调用,要么每次查询front时调用
void update_front_min(int id){
    while(!minq.empty() && minq.front().id<id){
        minq.pop_front();
    }
}

数论

数论

定理与推论

威尔逊定理: p为素数,则 p ∣ ( p − 1 ) ! + 1 p\mid(p-1)!+1 p(p1)!+1,则 ( p − 2 ) ! ≡ 1 ( m o d   p ) (p-2)!\equiv1(mod\space p) (p2)!1(mod p)

欧拉定理: p,a互素,则 a ϕ ( p ) ≡ 1 ( m o d   p ) a^{\phi(p)}\equiv1(mod\space p) aϕ(p)1(mod p)

费马小定理: p为素数,则 a p ≡ a ( m o d   p ) a^p\equiv a(mod\space p) apa(mod p)

推论:p为素数,当 p ∣ a , a p − 1 ≡ 0 ( m o d   p ) p\mid a,a^{p-1}\equiv0(mod\space p) pa,ap10(mod p);当 p ∤ a , a p − 1 ≡ 1 ( m o d   p ) p\nmid a,a^{p-1}\equiv 1(mod\space p) pa,ap11(mod p)

GCD性质: g c d ( a , b ) = g c d ( a − b , b ) gcd(a,b)=gcd(a-b,b) gcd(a,b)=gcd(ab,b)

GCD性质推论: g c d ( a 1 + k , a 2 + k , . . . . . . , a n + k ) = g c d ( a 1 + k , w ) gcd(a_1+k,a_2+k,......,a_n+k)=gcd(a_1+k,w) gcd(a1+k,a2+k,......,an+k)=gcd(a1+k,w),其中 w = g c d ( a 2 − a 1 , a 3 − a 2 , a 4 − a 3 , . . . . . . , a n − a n − 1 ) w=gcd(a_2-a_1,a_3-a_2,a_4-a_3,......,a_n-a_{n-1}) w=gcd(a2a1,a3a2,a4a3,......,anan1)

欧几里得算法

int gcd(int a,int b){
	return b==0?a:gcd(b,a%b);
}
int lcm(int a,int b){
	return a/gcd(a,b)*b;
}

扩展欧几里得算法

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

欧拉筛

// x以内的素数约有x/ln(x)个
const int maxn=1000;
bool number[maxn+5];
int prime[maxn+5];
void euler_sieve(){
	int i,j,cnt=0;
	memset(number,true,sizeof(number));
	memset(prime,0,sizeof(prime));
	for(int i=2;i<=maxn;i++){
		if(number[i]) prime[cnt++]=i;
		for(j=0;j<cnt&&prime[j]*i<=maxn;j++){
			number[prime[j]*i]=false;
			if(i%prime[j]==0) break; 
 		}
	}
}

求逆元

x y ≡ 1 ( m o d   p ) xy\equiv1(mod\space p) xy1(mod p),则称xy互为逆元

( n / a ) % p (n/a)\% p (n/a)%p等价于 ( n ∗ i n v ( a ) ) % p (n*inv(a))\% p (ninv(a))%p

// 扩展欧几里得法,通用
ll get_inv(ll a,ll mod){
	ll x,y;
	ll d=exgcd(a,mod,x,y);
	return d==1?(x%mod+mod)%mod:-1;
}
//费马小定理法,仅当mod为素数
ll prime_inv(ll a,ll mod){
	return mod_exp(a,mod-2,mod);
}
//线性法,用来求一组逆元
void linear_inv(int inv[],int len,int mod){
	inv[0]=inv[1]=1;
	for(int i=2;i<len;i++){
		inv[i]=((mod-mod/i)*inv[mod%i])%mod;
	}
}

中国剩余定理

ll china(ll a[],ll b[],int n)//a[]为除数,b[]为余数
{
    ll M=1,y,x=0;
    for(int i=0;i<n;++i)
        M*=a[i];
    for(int i=0;i<n;++i)
    {
        ll w=M/a[i];
        ll tx=0;
        int t=exgcd(w,a[i],tx,y); 
        x=(x+w*(b[i]/t)*x)%M; 
    }
    return (x+M)%M;
}

大素数

miller-rabin判断大素数

ll mod_mul(ll a,ll b,ll n){
	a%=n; //优化后只要在开头模一次,其他的地方用if,效率提升近一倍
	b%=n;
    ll res=0;
    while(b){
        if(b&1){
			res+=a;
			if(res>n) res-=n;
		}
		a+=a;
		if(a>n) a-=n;
        b/=2; //没开o2的话要换成b>>=1
    }
    return res;
}

ll mod_exp(ll a,ll b,ll n){
    ll res=1;
	a=a%n;
    while(b){
        if(b&1) res=mod_mul(res,a,n);
        a=mod_mul(a,a,n);
        b/=2;
    }
    return res;
}

bool miller_rabin(ll n){
    if(n==2||n==3||n==5||n==7||n==11)return true;
    if(n==1||!(n%2)||!(n%3)||!(n%5)||!(n%7)||!(n%11))return false;
    ll x,pre,u;
    int i,j,k=0;
    u=n-1;
    while(!(u&1)){
        k++;u>>=1;
    }
    srand((ll)time(0));
    for(i=0;i<8;i++){ //8~10次足够
        x=rand()%(n-2)+2;
        if((x%n)==0)continue;
        x=mod_exp(x,u,n);
        pre=x;
        for(j=0;j<k;j++){
            x=mod_mul(x,x,n);
            if(x==1&&pre!=1&&pre!=n-1)return false;
            pre=x;
        }
        if(x!=1)return false;
    }
    return true;
}

大素数之和

求十亿内所有质数的和,怎么做最快? - PlanarG的回答 - 知乎

Min25筛实现,大约每100ms算一个

const int N = 1e6 + 10;//对应素数精度可以到1e10

typedef __int128 LL;//防止1e10 * 1e10爆long long

namespace Min25
{

    int prime[N], id1[N], id2[N], flag[N], ncnt, m;

    LL g[N], sum[N], a[N], T, n;

    inline int ID(LL x)
    {
        return x <= T ? id1[x] : id2[n / x];
    }

    inline LL calc(LL x)
    {
        return x * (x + 1) / 2 - 1;
    }

    inline LL f(LL x)
    {
        return x;
    }

    inline void init()
    {
        ncnt = m = T = 0;

        T = sqrt(n + 0.5);
        for (int i = 2; i <= T; i++)
        {
            if (!flag[i])
                prime[++ncnt] = i, sum[ncnt] = sum[ncnt - 1] + i;
            for (int j = 1; j <= ncnt && i * prime[j] <= T; j++)
            {
                flag[i * prime[j]] = 1;
                if (i % prime[j] == 0)
                    break;
            }
        }
        for (LL l = 1; l <= n; l = n / (n / l) + 1)
        {
            a[++m] = n / l;
            if (a[m] <= T)
                id1[a[m]] = m;
            else
                id2[n / a[m]] = m;
            g[m] = calc(a[m]);
        }
        for (int i = 1; i <= ncnt; i++)
            for (int j = 1; j <= m && (LL)prime[i] * prime[i] <= a[j]; j++)
                g[j] = g[j] - (LL)prime[i] * (g[ID(a[j] / prime[i])] - sum[i - 1]);
    }

    inline LL solve(LL x)
    {
        if (x <= 1)
            return x;
        n = x;
        init();
        return g[ID(n)];
    }

} // namespace Min25

int main() {
    LL n; scanf("%lld", &n);
    printf("%lld\n", Min25::solve(n));
}

大素数个数

Meissel Lehmer Algorithm 求前n个数中素数个数 【模板】

#include <bits/stdtr1c++.h>
  
#define MAXN 100
#define MAXM 10001
#define MAXP 40000
#define MAX 400000
#define clr(ar) memset(ar, 0, sizeof(ar))
#define read() freopen("lol.txt", "r", stdin)
#define dbg(x) cout << #x << " = " << x << endl
#define chkbit(ar, i) (((ar[(i) >> 6]) & (1 << (((i) >> 1) & 31))))
#define setbit(ar, i) (((ar[(i) >> 6]) |= (1 << (((i) >> 1) & 31))))
#define isprime(x) (( (x) && ((x)&1) && (!chkbit(ar, (x)))) || ((x) == 2))
  
using namespace std;
  
namespace pcf{
    long long dp[MAXN][MAXM];
    unsigned int ar[(MAX >> 6) + 5] = {0};
    int len = 0, primes[MAXP], counter[MAX];
  
    void Sieve(){
        setbit(ar, 0), setbit(ar, 1);
        for (int i = 3; (i * i) < MAX; i++, i++){
            if (!chkbit(ar, i)){
                int k = i << 1;
                for (int j = (i * i); j < MAX; j += k) setbit(ar, j);
            }
        }
  
        for (int i = 1; i < MAX; i++){
            counter[i] = counter[i - 1];
            if (isprime(i)) primes[len++] = i, counter[i]++;
        }
    }
  
    void init(){
        Sieve();
        for (int n = 0; n < MAXN; n++){
            for (int m = 0; m < MAXM; m++){
                if (!n) dp[n][m] = m;
                else dp[n][m] = dp[n - 1][m] - dp[n - 1][m / primes[n - 1]];
            }
        }
    }
  
    long long phi(long long m, int n){
        if (n == 0) return m;
        if (primes[n - 1] >= m) return 1;
        if (m < MAXM && n < MAXN) return dp[n][m];
        return phi(m, n - 1) - phi(m / primes[n - 1], n - 1);
    }
  
    long long Lehmer(long long m){
        if (m < MAX) return counter[m];
  
        long long w, res = 0;
        int i, a, s, c, x, y;
        s = sqrt(0.9 + m), y = c = cbrt(0.9 + m);
        a = counter[y], res = phi(m, a) + a - 1;
        for (i = a; primes[i] <= s; i++) res = res - Lehmer(m / primes[i]) + Lehmer(primes[i]) - 1;
        return res;
    }
}
  
long long solve(long long n){
    int i, j, k, l;
    long long x, y, res = 0;
  
    for (i = 0; i < pcf::len; i++){
        x = pcf::primes[i], y = n / x;
        if ((x * x) > n) break;
        res += (pcf::Lehmer(y) - pcf::Lehmer(x));
    }
  
    for (i = 0; i < pcf::len; i++){
        x = pcf::primes[i];
        if ((x * x * x) > n) break;
        res++;
    }
  
    return res;
}
  
int main(){
    pcf::init();
    long long n, res;
  
    while (scanf("%lld", &n) != EOF){
        //res = solve(n);
        printf("%lld\n",pcf::Lehmer(n));
        //printf("%lld\n", res);
    }
    return 0;
}

大数分解质因数

// 2020 CCPC 威海站 D ABC Conjecture
// 给定一个不超1e18的数,判断它有没有形如x^2形式(x为任意整数)的因子

ll factor[100]; //质因数分解结果,无序的
int tol; // 质因子个数,数组下标从0~tol-1

ll gcd(ll a,ll b){
	ll t;
	while(b){
		t=a;
		a=b;
		b=t%b;
	}
	return a>=0?a:-a; // pollard_rho专用的gcd
}

// 找出一个因子
ll pollard_rho(ll x,ll c){
	ll i=1,k=2;
	srand(time(NULL));
	ll x0=rand()%(x-1)+1;
	ll y=x0;
	while(1){
		i++;
		x0=(mod_mul(x0,x0,x)+c)%x;
		ll d=gcd(y-x0,x);
		if(d!=1 && d!=x) return d;
		if(y==x0) return x;
		if(i==k) {
			y=x0;
			k+=k;
		}
	}
}

// 对n分解素因子,因子存在factor里,k一般设为107
void findfac(ll n,int k){
	if(n==1) return;
	if(miller_rabin(n)){
		factor[tol++]=n;
		return;
	}
	ll p=n;
	int c=k;
	while(p>=n){
		p=pollard_rho(p, c--); //值变化,防止k死循环
	}
	findfac(p,k);
	findfac(n/p,k);
}

bool solve(ll n){
	if(miller_rabin(n)){ // 素数一定无法被分解
		return false;
	}
	else {
		tol=0;
		findfac(n,107);
		ll sq=sqrt(n);
		for(int i=0;i<tol;i++){
			if(factor[i]>sq) continue;
			if(n%(factor[i]*factor[i])==0){
				return true;
			}
		}
	}
	return false;
}

int main(){
	int T;
	scanf("%d",&T);
	while(T--){
		ll n;
		scanf("%lld",&n);
		printf(solve(n)?"yes\n":"no\n");
	}
	return 0;
}

杂项

里技

记载了各种黑魔法。

调试宏

#define DEBUG
#define plog if(DEBUG) cout

速度上是没有损失的,我自己看过反汇编的代码,if(0)的时候确实被优化掉了。原因参见https://stackoverflow.com/questions/14657627/c-attempt-to-optimize-code-by-replacing-tests/14657645#14657645

大意:if 表达式的内容为编译时可确定的常量时,那么即可保证在开启优化编译的情况下 if(0) 这种代码会被直接删除。

随机骗分

ICPC 2020 济南 J

对于范围比较小的构造题,在实在想不出来,有没有别的题可做的时候,可以尝试用随机代码骗分。

do{ rebuild(); }while(!check());

思想

二分法

用于在单调函数f上找最接近x的值。

while(l<r){
    mid=(l+r)/2;
    if(f(mid)>x) r=mid;
    else l=mid+1;
}

三分法

用于找凹函数f的最小值。凸函数时改成大于号。

对于浮点数,只要去掉+1,-1,并把l<r改成fabs(l-r)>eps即可。别在f(mid1)<f(mid2)里加fabs()>eps

while(l<r){
	int mid1=l+(r-l)/3,mid2=r-(r-l)/3;
    if(f(mid1)<f(mid2)) r=mid2-1;
    else l=mid1+1;
}

C++扩展

自定义比较器

bool cmp(const int& x, const int& y){return x<y;}

int128读写

只能在g++下使用

inline __int128 read(){
    __int128 x=0,f=1;
    char ch=getchar();
    while(ch<'0'||ch>'9'){
        if(ch=='-')
            f=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        x=x*10+ch-'0';
        ch=getchar();
    }
    return x*f;
}

inline void print(__int128 x){
    if(x<0){
        putchar('-');
        x=-x;
    }
    if(x>9)
        print(x/10);
    putchar(x%10+'0');
}

概率论

概率论

二项分布

事件发生概率为 p p p,n次重复试验中事件恰好发生k次的概率

P { X = k } = C n k p k ( 1 − p ) n − k P\{X=k\}=C_n^kp^k(1-p)^{n-k} P{X=k}=Cnkpk(1p)nk

E ( X ) = n p E(X)=np E(X)=np

D ( X ) = n p ( 1 − p ) D(X)=np(1-p) D(X)=np(1p)

几何分布

事件发生概率为p,重复试验直到事件出现为止,此时所进行实验次数为X

P { X = k } = ( 1 − p ) k − 1 p , k = 1 , 2 , . . . P\{X=k\}=(1-p)^{k-1}p,k=1,2,... P{X=k}=(1p)k1p,k=1,2,...

为得到一次成功进行n次试验,n的概率分布:

E ( X ) = 1 p E(X)=\frac 1 p E(X)=p1

D ( X ) = 1 − p p 2 D(X)=\frac {1-p} p^2 D(X)=p1p2

m=n-1次失败,第n次成功,m的概率分布:

E ( X ) = 1 − p p E(X)=\frac {1-p} p E(X)=p1p

D ( X ) = 1 − p p 2 D(X)=\frac {1-p} p^2 D(X)=p1p2

超几何分布

N件产品中M件不合格,从中随机选n件做检查,发现k件不合格品的概率

P { X = k } = C M k C N − M n − k C N n P\{X=k\}=\frac {C^k_MC^{n-k}_{N-M}} {C^n_N} P{X=k}=CNnCMkCNMnk

E ( X ) = n M N E(X)=\frac {nM} N E(X)=NnM

D ( X ) = n M N ( 1 − M N ) N − n N − 1 D(X)=\frac{nM}{N}(1-\frac M N)\frac {N-n}{N-1} D(X)=NnM(1NM)N1Nn

期望花费

若某个事件概率为p,成功时花费为a,失败时的花费为b。则对于以上的几种分布,都有期望公式:

E ( c o s t ) = [ a ∗ 1 p + b ∗ ( 1 − 1 p ) ] ∗ E ( X ) E(cost)=[a *\frac 1 p + b *(1-\frac 1 p)]*E(X) E(cost)=[ap1+b(1p1)]E(X)

线性代数

高斯消元

用于求类线性方程组的解。

参考资料线性代数 —— 高斯消元法

结果分三种:

  • 唯一解
  • 无解
  • 多组解:结果中存在自由元。对于整数方程和浮点数方程来说,此时相当于无数种解。对于异或方程来说,此时有2^自由元个数个解。

比如对于矩阵

a b c val
1 2 3 1
0 0 0 0
0 0 0 0

至少确定a,b,c中的两个才能确定最终解,所以有2个自由元,有无穷多组解。

浮点线性方程组

double a[N][N];//增广矩阵
double x[N];//解集
bool freeX[N];//标记是否为自由变元
 
int Gauss(int equ,int var){//返回自由变元个数
    /*初始化*/
    for(int i=0;i<=var;i++){
        x[i]=0;
        freeX[i]=true;
    }
 
    /*转换为阶梯阵*/
    int col=0;//当前处理的列
    int row;//当前处理的行
    for(row=0;row<equ&&col<var;row++,col++){//枚举当前处理的行
        int maxRow=row;//当前列绝对值最大的行
        for(int i=row+1;i<equ;i++){//寻找当前列绝对值最大的行
            if(fabs(a[i][col])>fabs(a[maxRow][col]))
                maxRow=i;
        }
        if(maxRow!=row){//与第row行交换
            for(int j=row;j<var+1;j++)
                swap(a[row][j],a[maxRow][j]);
        }
        if(fabs(a[row][col])<1e6){//col列第row行以下全是0,处理当前行的下一列
            row--;
            continue;
        }
 
        for(int i=row+1;i<equ;i++){//枚举要删去的行
            if(fabs(a[i][col])>1e6){
                double temp=a[i][col]/a[row][col];
                for(int j=col;j<var+1;j++) 
                    a[i][j]-=a[row][j]*temp;
                a[i][col]=0;
            }
        }
    }
 
    /*求解*/
    //无解
    for(int i=row;i<equ;i++)
        if(fabs(a[i][col])>1e6)
            return -1;
 
    //无穷解: 在var*(var+1)的增广阵中出现(0,0,...,0)这样的行
    int temp=var-row;//自由变元有var-row个
    if(row<var)//返回自由变元数
        return temp;
 
    //唯一解: 在var*(var+1)的增广阵中形成严格的上三角阵
    for(int i=var-1;i>=0;i--){//计算解集
        double temp=a[i][var];
        for(int j=i+1;j<var;j++)
            temp-=a[i][j]*x[j];
        x[i]=temp/a[i][i];
    }
    return 0;
}

模线性方程组

int a[N][N];//增广矩阵
int x[N];//解集
bool freeX[N];//标记是否为自由变元
int GCD(int a,int b){
    return !b?a:GCD(b,a%b);
}
int LCM(int a,int b){
    return a/GCD(a,b)*b;
}
int Gauss(int equ,int var){//返回自由变元个数
    /*初始化*/
    for(int i=0;i<=var;i++){
        x[i]=0;
        freeX[i]=true;
    }
 
    /*转换为阶梯阵*/
    int col=0;//当前处理的列
    int row;//当前处理的行
    for(row=0;row<equ&&col<var;row++,col++){//枚举当前处理的行
        int maxRow=row;//当前列绝对值最大的行
        for(int i=row+1;i<equ;i++){//寻找当前列绝对值最大的行
            if(abs(a[i][col])>abs(a[maxRow][col]))
                maxRow=i;
        }
        if(maxRow!=row){//与第row行交换
            for(int j=row;j<var+1;j++)
                swap(a[row][j],a[maxRow][j]);
        }
        if(a[row][col]==0){//col列第row行以下全是0,处理当前行的下一列
            row--;
            continue;
        }
 
        for(int i=row+1;i<equ;i++){//枚举要删去的行
            if(a[i][col]!=0){
                int lcm=LCM(abs(a[i][col]),abs(a[row][col]));
                int ta=lcm/abs(a[i][col]);
                int tb=lcm/abs(a[row][col]);
                if(a[i][col]*a[row][col]<0)//异号情况相加
                    tb=-tb;
                for(int j=col;j<var+1;j++) {
                    a[i][j]=((a[i][j]*ta-a[row][j]*tb)%MOD+MOD)%MOD;
                }
            }
        }
    }
 
    /*求解*/
    //无解:化简的增广阵中存在(0,0,...,a)这样的行,且a!=0
    for(int i=row;i<equ;i++)
        if (a[i][col]!=0)
            return -1;
 
    //无穷解: 在var*(var+1)的增广阵中出现(0,0,...,0)这样的行
    int temp=var-row;//自由变元有var-row个
    if(row<var)//返回自由变元数
        return temp;
 
    //唯一解: 在var*(var+1)的增广阵中形成严格的上三角阵
    for(int i=var-1;i>=0;i--){//计算解集
        int temp=a[i][var];
        for(int j=i+1;j<var;j++){
            if(a[i][j]!=0)
                temp-=a[i][j]*x[j];
            temp=(temp%MOD+MOD)%MOD;//取模
        }
        while(temp%a[i][i]!=0)//外层每次循环都是求a[i][i],它是每个方程中唯一一个未知的变量
            temp+=MOD;//a[i][i]必须为整数,加上周期MOD
        x[i]=(temp/a[i][i])%MOD;//取模
    }
    return 0;
}

异或方程组

用于解形如a1x1^a2x2^a3x3...=b的方程。

例题:2020 ICPC 济南 A Matrix Equation

int a[N][N];//增广矩阵
int x[N];//解集
int freeX[N];//自由变元
int freeX[N];//自由变元
// equ:方程个数 var:变量个数
int Gauss(int equ,int var){//返回自由变元个数
	/*初始化*/
	for(int i=0;i<=var;i++){
		x[i]=0;
		freeX[i]=0;
	}
 
	/*转换为阶梯阵*/
	int col=0;//当前处理的列
	int num=0;//自由变元的序号
	int k;//当前处理的行
	for(k=0;k<equ&&col<var;k++,col++){//枚举当前处理的行
		int maxr=k;//当前列绝对值最大的行
		for(int i=k+1;i<equ;i++){//寻找当前列绝对值最大的行
			if(a[i][col]>a[maxr][col]){
				maxr=i;
				swap(a[k],a[maxr]);//与第k行交换
				break;
			}
		}
		if(a[k][col]==0){//col列第k行以下全是0,处理当前行的下一列
			freeX[num++]=col;//记录自由变元
			k--;
			continue;
		}
 
		for(int i=k+1;i<equ;i++){
			if(a[i][col]!=0){
				for(int j=col;j<var+1;j++){//对于下面出现该列中有1的行,需要把1消掉
					a[i][j]^=a[k][j];
				}
			}
		}
	}
 
	/*求解*/
	//无解:化简的增广阵中存在(0,0,...,a)这样的行,且a!=0
	for(int i=k;i<equ;i++)
		if(a[i][col]!=0)
			return -1;
 
	//无穷解: 在var*(var+1)的增广阵中出现(0,0,...,0)这样的行
	if(k<var)//返回自由变元数
		return var-k;//自由变元有var-k个
 
	//唯一解: 在var*(var+1)的增广阵中形成严格的上三角阵
	for(int i=var-1;i>=0;i--){//计算解集
		x[i]=a[i][var];
		for(int j=i+1;j<var;j++)
			x[i]^=(a[i][j]&&x[j]);
	}
	return 0;
}

组合数学

公式

等差数列公式

a n = a 1 + ( n − 1 ) ∗ d a_n = a_1 + (n-1) * d an=a1+(n1)d

S n = n a 1 + n ( n − 1 ) 2 d = n ( a 1 + a n ) 2 , n ∈ N ∗ S_n = na_1 + \frac{n(n-1)}{2}d = \frac {n(a_1+a_n)}{2}, n\in N^* Sn=na1+2n(n1)d=2n(a1+an),nN

等比数列公式

a n = a 1 ∗ q ( n − 1 ) a_n = a_1*q^{(n-1)} an=a1q(n1)

S n = a 1 ( 1 − q n ) 1 − q ( q ≠ 1 ) S_n = \frac {a_1(1-q^n)}{1-q}(q\neq1) Sn=1qa1(1qn)(q=1)

组合数公式

C n k = n ! ( n − m ) ! m ! C_n^k=\frac{n!}{(n-m)!m!} Cnk=(nm)!m!n!

组合数模板

组合数之打表法,N<=3000

//组合数打表模板,适用于N<=3000
//c[i][j]表示从i个中选j个的选法。
const int N=33; 
long long C[N][N];
 
void get_C(int maxn)
{
    C[0][0] = 1;
    for(int i=1;i<=maxn;i++)
    {
        C[i][0] = 1;
        for(int j=1;j<=i;j++)
            C[i][j] = C[i-1][j]+C[i-1][j-1];
            //C[i][j] = (C[i-1][j]+C[i-1][j-1])%MOD;
    }
}

组合数之求单个值 N<=2e5

#include <iostream>
#include <bits/stdc++.h>
#define maxn 200005
typedef long long ll;
using namespace std;
const ll mod=998244353;
ll fac[maxn],inv[maxn];
ll pow_mod(ll a,ll n)
{
    ll ret =1;
    while(n)
    {
        if(n&1) ret=ret*a%mod;
          a=a*a%mod;
          n>>=1;
    }
    return ret;
}
void init()
{
    fac[0]=1;
    for(int i=1;i<maxn;i++)
    {
        fac[i]=fac[i-1]*i%mod;
    }
}
ll Cc(ll x, ll y)
{
    return fac[x]*pow_mod(fac[y]*fac[x-y]%mod,mod-2)%mod;
}
 
int main(){
    ll n,m;
    init();
    while(1){
        cin>>n>>m;
        cout<<Cc(n,m)<<endl;
    }
}

隔板法

大前提:各个元素是相同的。

x + y + z = k x+y+z=k x+y+z=k解的个数:可以隔板法。

n n n种颜色球分成 k k k组:不行隔板法。

一、不为0的隔板法

n n n个球,分成 k k k组,每组至少有一个: C n − 1 k − 1 C_{n-1}^{k-1} Cn1k1

相当于在 n − 1 n-1 n1个空格里插入 k − 1 k-1 k1个隔板

二、可以为0的隔板法

n n n个球,分成 k k k组,每组可以为 0 0 0 C n + k − 1 k − 1 C_{n+k-1}^{k-1} Cn+k1k1

相当于为每一组预想加一个球,然后就转化成了第一种情况

三、每组有最低限制的隔板法

10 10 10个球放 3 3 3个组,第一组至少 1 1 1个,第二组至少 3 3 3个,第三组至少 0 0 0个。

给第一组第二组预先放好满足最低要求的球数,则转换成了第二种情况

C 10 + 3 − 1 − 1 − 3 3 − 1 = C 8 2 C_{10+3-1-1-3}^{3-1}=C_{8}^{2} C10+311331=C82

错排

n个编号箱子n个编号球,求有多少种方案满足有k个箱子和它内部的球编号不同。

D 1 = 0 D_1=0 D1=0

D 2 = 1 D_2=1 D2=1

D n = ( n − 1 ) ( D n − 1 + D n − 2 ) D_n=(n-1)(D_{n-1}+D_{n-2}) Dn=(n1)(Dn1+Dn2)

通项公式: D n = n ! [ 1 2 ! − 1 3 ! + 1 4 ! − . . . + ( − 1 ) n 1 n ! ] D_n=n![\frac{1}{2!}-\frac{1}{3!}+\frac{1}{4!}-...+(-1)^n\frac{1}{n!}] Dn=n![2!13!1+4!1...+(1)nn!1]

原文链接
如有侵权请联系删除

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值