动态规划
背包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进栈里
若新元素小于等于栈顶,则二分找到第一个比它大的元素,并替换之
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;
}
对于最长不下降子序列,只要将s.top() < x
替换为s.top()<=x
,将lower_bound
换成upper_bound
即可
对于最长下降子序列,把s.top()<x
换成>
,把lower_bound
的比较器换成greater<int>()
Dilworth定理
最少可分割为多少个不上升子序列等于最长上升子序列长度
各元素不重复的最长公共子序列
将字符串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,n−l)。部分题目区分左右端点的情况,可以扩展成 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的顺序嵌套遍历。
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]);
}
}
}
遍历有效子节点个数问题
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)+(N−child[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) (N−child[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]+(N−2∗child[u]−2)∗dis(u,fa)
树上哈希
题目大意:给你 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+∑y∈sonxfy×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,访问子节点前调用
数位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]两条边。
对于x与y中至多一个为真的问题,可连边x->y^1
和y->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;
}
单调队列
用来解决求滑动窗口中的每一节上的最大值/最小值
下面以单调上升队列为例
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∣(p−1)!+1,则 ( p − 2 ) ! ≡ 1 ( m o d p ) (p-2)!\equiv1(mod\space p) (p−2)!≡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) ap≡a(mod p)
推论:p为素数,当 p ∣ a , a p − 1 ≡ 0 ( m o d p ) p\mid a,a^{p-1}\equiv0(mod\space p) p∣a,ap−1≡0(mod p);当 p ∤ a , a p − 1 ≡ 1 ( m o d p ) p\nmid a,a^{p-1}\equiv 1(mod\space p) p∤a,ap−1≡1(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(a−b,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(a2−a1,a3−a2,a4−a3,......,an−an−1)
欧几里得算法
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) xy≡1(mod p),则称xy互为逆元
( n / a ) % p (n/a)\% p (n/a)%p等价于 ( n ∗ i n v ( a ) ) % p (n*inv(a))\% p (n∗inv(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)
这种代码会被直接删除。
随机骗分
对于范围比较小的构造题,在实在想不出来,有没有别的题可做的时候,可以尝试用随机代码骗分。
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(1−p)n−k
E ( X ) = n p E(X)=np E(X)=np
D ( X ) = n p ( 1 − p ) D(X)=np(1-p) D(X)=np(1−p)
几何分布
事件发生概率为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}=(1−p)k−1p,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)=p1−p2
m=n-1次失败,第n次成功,m的概率分布:
E ( X ) = 1 − p p E(X)=\frac {1-p} p E(X)=p1−p
D ( X ) = 1 − p p 2 D(X)=\frac {1-p} p^2 D(X)=p1−p2
超几何分布
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}=CNnCMkCN−Mn−k
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(1−NM)N−1N−n
期望花费
若某个事件概率为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)=[a∗p1+b∗(1−p1)]∗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+(n−1)∗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(n−1)d=2n(a1+an),n∈N∗
等比数列公式
a n = a 1 ∗ q ( n − 1 ) a_n = a_1*q^{(n-1)} an=a1∗q(n−1)
S n = a 1 ( 1 − q n ) 1 − q ( q ≠ 1 ) S_n = \frac {a_1(1-q^n)}{1-q}(q\neq1) Sn=1−qa1(1−qn)(q=1)
组合数公式
C n k = n ! ( n − m ) ! m ! C_n^k=\frac{n!}{(n-m)!m!} Cnk=(n−m)!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} Cn−1k−1
相当于在 n − 1 n-1 n−1个空格里插入 k − 1 k-1 k−1个隔板
二、可以为0的隔板法
n n n个球,分成 k k k组,每组可以为 0 0 0: C n + k − 1 k − 1 C_{n+k-1}^{k-1} Cn+k−1k−1
相当于为每一组预想加一个球,然后就转化成了第一种情况
三、每组有最低限制的隔板法
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+3−1−1−33−1=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=(n−1)(Dn−1+Dn−2)
通项公式: 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!1−3!1+4!1−...+(−1)nn!1]
原文链接
如有侵权请联系删除