部分参照这一篇 【浮*光】 #noip总复习# BY.hss
注意省选题常见的思路:
三分极值 / 二分答案
贪心 + 多数组的转移转化
正逆序处理:倒推法
找规律,确定单调性
多区间问题的处理:倍增法(RMQ)
【重点中的重点】
(1)离散化
int kt[N],a[N]; //辅助数组kt[] int main(){ for(int i=1;i<=n;i++) cin>>a[i],kt[i]=a[i]; sort(kt+1,kt+n+1); //辅助数组进行排序 m=unique(kt+1,kt+n+1)-kt-1; //注意要-kt-1 for(int i=1;i<=n;i++) //↓↓第一个大于等于a[i]的位置 a[i]=lower_bound(kt+1,kt+m+1,a[i])-kt; //注意只用-kt }
(2)线段树
struct SegmentTree{ int l,r,sum; }tree[4*N]; void PushUp(int rt){ tree[rt].sum=tree[rt<<1].sum+tree[rt<<1|1].sum; } void build(int l,int r,int rt){ //【建树】 tree[rt].l=l; tree[rt].r=r; //建立标号与区间的关系 if(l==r){ scanf("%d",&tree[rt].sum); return; } //叶子节点 int mid=(l+r)/2; build(l,mid,rt<<1),build(mid+1,r,rt<<1|1); PushUp(rt); //将修改值向上传递 } void add(int p,int rt){ //【单点修改】 if(tree[rt].l==tree[rt].r){ tree[rt].sum+=y; return; } //叶子节点 int mid=(tree[rt].l+tree[rt].r)>>1; if(p<=mid) add(p,rt<<1); else add(p,rt<<1|1); tree[rt].sum=tree[rt<<1].sum+tree[rt<<1|1].sum; //pushup } void query(int p,int rt){ //【单点查询】 if(tree[rt].l==tree[rt].r){ ans=tree[rt].sum; return; } //叶子节点 int mid=(tree[rt].l+tree[rt].r)>>1; if(p<=mid) query(p,rt<<1); else query(p,rt<<1|1); } void sum(int rt){ //【区间查询求和】 if(tree[rt].l>=x&&tree[rt].r<=y) //区间完全包含 { ans+=tree[rt].sum; return; } int mid=(tree[rt].l+tree[rt].r)>>1; if(x<=mid) sum(rt<<1); //区间部分重叠,递归左右 if(y>=mid+1) sum(rt<<1|1); }
void PushDown(int l,int r,int rt){ //tag是区间修改的标记 int mid=(l+r)>>1; if(tree[rt].tag==-1||l==r) return; tree[rt<<1].tag=tree[rt<<1|1].tag=tree[rt].tag, tree[rt<<1].ans=(tree[rt].tag==0)?(mid-l+1):0; tree[rt<<1|1].ans=(tree[rt].tag==0)?(r-mid):0; tree[rt<<1].l=tree[rt<<1].r=tree[rt<<1].ans; tree[rt<<1|1].l=tree[rt<<1|1].r=tree[rt<<1|1].ans; tree[rt].tag=-1; //标记每次下移一位,并清空上一位置的标记 }
(3)树状数组
void add(ll x,ll k) //单点修改、维护前缀和 { for(i=x;i<=n;i+=i&-i) c[i]+=k; } ll query(ll x) //区间查询、查询前缀和 { ll sum=0; for(i=x;i>0;i-=i&-i) sum+=c[i]; return sum; }
(4)主席树
主程序中: for(int i=1;i<=n;i++) //对应颜色的位置上+1 reads(col[i]),update(rt[i-1],rt[i],1,MAX,col[i],1); void update(int las_,int &now_,int l,int r,int val,int op){ now_=++node_cnt; //主席树动态开点:寻找新值val的位置,建出这条新链 ls[now_]=ls[las_],rs[now_]=rs[las_],sum[now_]=sum[las_]+op; if(l==r) return; int mid=(l+r)>>1; if(val<=mid) update(ls[las_],ls[now_],l,mid,val,op); else update(rs[las_],rs[now_],mid+1,r,val,op); } int query(int u,int v,int l,int r,int k){ //查询区间中颜色k的个数 if(l==r) return sum[v]-sum[u]; int mid=(l+r)>>1; if(k<=mid) return query(ls[u],ls[v],l,mid,k); else return query(rs[u],rs[v],mid+1,r,k); //递归子树寻找位置k } //query(rt[l-1],rt[r],1,MAX,k) //寻找区间[l,r]中颜色k的个数
(5)Trie
bool tail[SIZE]; //标记串尾元素 int trie[SIZE][26],tot=1; //SIZE:字符串最大长度(层数) //tot为节点编号,用它可以在trie数组中表示某层的某字母是否存在 void insert(char* ss){ //插入一个字符串 int len=strlen(ss),p=1; //p初始化为根节点1 for(int k=0;k<len;k++){ int ch=ss[k]-'a'; //小写字符组成串的某个字符,变成数字 if(trie[p][ch]==0) trie[p][ch]=++tot; //trie存编号tot //↑↑↑不存在此层的这个字符,新建结点,转移边 p=trie[p][ch]; //指针移动,连接下一个位置 } tail[p]=true; //s中字符扫描完毕,tail标记字符串的末位字符(的编号p) } bool searchs(char* ss){ //检索字符串是否存在 int len=strlen(ss),p=1; //p初始化为根节点 for(int k=0;k<len;k++){ p=trie[p][ss[k]-'a']; //寻找下一处字符 if(p==0) return false; //某层字符没有编号,不存在,即串也不存在 } return tail[p]; //判断最后一个字符所在的位置是否是某单词的末尾 }
(6)KMP
void pre(){ //【预处理nextt[i]】 nextt[1]=0; int j=0; //j指针初始化为0 for(int i=1;i<m;i++){ //a数组自我匹配,从i+1=2与1比较开始 while(j>0&&a[i+1]!=a[j+1]) j=nextt[j]; //↑自身无法继续匹配且j还没减到0,考虑返回匹配的剩余状态 if(a[i+1]==a[j+1]) j++; //这一位匹配成功 nextt[i+1]=j; //记录这一位向前的最长匹配 } } void kmp(){ //在b串中寻找a串出现的位置 int ans=0,j=0; for(int i=0;i<n;i++){ //扫描b,寻找a的匹配 while(b[i+1]!=a[j+1]&&j>0) j=nextt[j]; //↑不能继续匹配且j还没减到0(之前的匹配有剩余状态) if(b[i+1]==a[j+1]) j++; //匹配加长,j++ if(j==m){ //【一定要把这个判断写在j++的后面!】 printf("%d ",i+1-m+1); //子串a的起点在母串b中的位置 j=nextt[j]; //继续寻找匹配 } //【↑↑巧妙↑↑这里不用返回0,只用返回上一匹配值】 } //注意:如果询问串的不重叠出现次数,则j必须变成0 }
(7)后缀数组
const int maxn=500019; int n,m; char s[maxn]; int rank[maxn],b[maxn],sa[maxn],tp[maxn],tax[maxn],height[maxn]; void qsort(){ //(1)基数排序 for(int i=0;i<=m;i++) tax[i]=0; for(int i=1;i<=n;i++) tax[rank[i]]++; for(int i=1;i<=m;i++) tax[i]+=tax[i-1]; for(int i=n;i>=1;i--) sa[tax[rank[tp[i]]]--]=tp[i]; } void SuffixSort(){ //(2)后缀排序 for(int i=1;i<=n;i++) rank[i]=s[i],tp[i]=i; m=519; qsort(); //第一次基数排序 for(int k=1;k<=n;k<<=1){ int p=0; for(int i=n-k+1;i<=n;i++) tp[++p]=i; //for(int i=1;i<=k;i++) tp[++p]=n-k+i; for(int i=1;i<=n;i++) if(sa[i]>k) tp[++p]=sa[i]-k; qsort(); // swap(rank,tp); for(int i=1;i<=n;i++) //注意此处数组交换的方式 b[i]=tp[i],tp[i]=rank[i],rank[i]=b[i]; rank[sa[1]]=p=1; for(int i=2;i<=n;i++) rank[sa[i]]=(tp[sa[i]]==tp[sa[i-1]] &&tp[sa[i]+k]==tp[sa[i-1]+k])?p:++p; if(p>=n) break; m=p; } } void getH(){ //(3)求height[] int k=0; for(int i=1;i<=n;i++){ if(k) k--; int j=sa[rank[i]-1]; while(s[i+k]==s[j+k]) k++; height[rank[i]]=k; } } //注意:此模板字符串从1位置开始,即scanf("%s",s+1);
(8)树链剖分
void dfs1(ll u,ll fa_){ //第一遍dfs:求子树大小和重儿子 siz[u]=1,fa[u]=fa_,dep[u]=dep[fa_]+1; for(ll i=head[u];i;i=e[i].nextt){ if(e[i].ver==fa_) continue; dfs1(e[i].ver,u),siz[u]+=siz[e[i].ver]; //计算size if(siz[e[i].ver]>siz[son[u]]) son[u]=e[i].ver; //重儿子 } } void dfs2(ll u,ll fa_){ //第二遍dfs:确定dfs序和top值 if(son[u]){ //先走重儿子,使重链上的各节点在线段树中的编号连续 seg[son[u]]=++seg[0]; //节点编号、记入线段树中 rev[seg[0]]=son[u]; //记录对应的原始编号 top[son[u]]=top[u],dfs2(son[u],u); //↑↑此位置是已有的重链的节点,更新top值,继续递归 } for(ll i=head[u];i;i=e[i].nextt){ //递归轻边 if(top[e[i].ver]) continue; //除去u的重儿子或父亲 seg[e[i].ver]=++seg[0],rev[seg[0]]=e[i].ver; //加入线段树 top[e[i].ver]=e[i].ver,dfs2(e[i].ver,u); //↑↑先递归到的轻边上的点(dep值min),所在重链的top一定是自己 } } ll query(ll x,ll y){ //路径询问 ll fx=top[x],fy=top[y],ans=0; while(fx!=fy){ //不在同一重链上,选择深度较大的跳到重链top的fa if(dep[fx]<dep[fy]) swap(x,y),swap(fx,fy); ans=ans+get(1,1,seg[0],seg[fx],seg[x]); //边跳边统计答案 x=fa[fx],fx=top[x]; //往上跳、并更新当前所在点的top值(所在重链) } if(dep[x]>dep[y]) swap(x,y); //x、y已在同一条重链上 ans=ans+get(1,1,seg[0],seg[x],seg[y]); return ans; //直接统计 }
(9)网络流最大流
- 最小割 = 最大流 ;(有正负权值时)最大权闭合子图 = s连的正权值之和 - min_cut 。
int s,t,tot=1,n,m,ans,head[N],dep[N],cur[N]; //s为源点,t为汇点 struct node{ int nextt,ver,w; }e[M]; void add(int x,int y,int z) { e[++tot].ver=y,e[tot].nextt=head[x],e[tot].w=z,head[x]=tot; } bool bfs(){ memset(dep,0,sizeof(dep)); //dep记录深度 memcpy(cur,head,sizeof(head)); queue<int> q; while(!q.empty()) q.pop(); dep[s]=1; q.push(s); while(!q.empty()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=e[i].nextt) if((e[i].w>0)&&(dep[e[i].ver]==0)) //分层 dep[e[i].ver]=dep[u]+1,q.push(e[i].ver); } if(dep[t]!=0) return 1; else return 0; //此时不存在分层图也不存在增广路 } int dfs(int u,int lastt){ int cnt=0; if(u==t) return lastt; //lastt:此点还剩余的流量 for(int i=cur[u];i&&cnt<lastt;i=e[i].nextt){ cur[u]=i; //当前弧优化 if((dep[e[i].ver]==dep[u]+1)&&(e[i].w!=0)){ int f=dfs(e[i].ver,min(lastt-cnt,e[i].w)); if(f>0){ e[i].w-=f,e[i^1].w+=f; cnt+=f; } } } if(cnt<lastt) dep[u]=-1; return cnt; } void dinic(){ ans=0; while(bfs()) ans+=dfs(s,2e9); }
(10)网络流费用流
struct edge{ ll ver,nextt,flow,cost; }e[2*N]; ll tot=-1,n,m,S,T,maxf=0,minc=0; ll flow[N],head[N],dist[N],inq[N],pre[N],lastt[N]; void add(ll a,ll b,ll f,ll c) { e[++tot].nextt=head[a],head[a]=tot, e[tot].ver=b,e[tot].flow=f,e[tot].cost=c; } bool spfa(ll S,ll T){ queue<ll> q; memset(inq,0,sizeof(inq)); memset(flow,0x7f,sizeof(flow)); memset(dist,0x7f,sizeof(dist)); q.push(S),dist[S]=0,pre[T]=-1,inq[S]=1; while(!q.empty()){ ll x=q.front(); q.pop(); inq[x]=0; for(ll i=head[x];i!=-1;i=e[i].nextt){ if(e[i].flow>0&&dist[e[i].ver]>dist[x]+e[i].cost){ dist[e[i].ver]=dist[x]+e[i].cost; pre[e[i].ver]=x,lastt[e[i].ver]=i; flow[e[i].ver]=min(flow[x],e[i].flow); if(!inq[e[i].ver]) q.push(e[i].ver),inq[e[i].ver]=1; } } } return pre[T]!=-1; } void mcmf(){ while(spfa(S,T)){ ll now=T; //↓↓最小费用最大流 maxf+=flow[T],minc+=dist[T]*flow[T]; while(now!=S){ //↓↓正边流量-,反边流量+ e[lastt[now]].flow-=flow[T]; e[lastt[now]^1].flow+=flow[T]; //↑↑利用xor1“成对储存”的性质 now=pre[now]; //维护前向边last,前向点pre } } } // 注意:tot=1,memset(head,-1,sizeof(head));
(11)二分图匹配
for(int i=1;i<=n;i++) //加入左侧每个节点,判断是否存在增广路 memset(vis,false,sizeof(vis)),ans+=dfs(i); //计算最大匹配边数 bool dfs(int x){ for(int i=head[x];i;i=e[i].nextt) //寻找连边 if(!vis[e[i].ver]){ //当前右节点在新左节点的匹配中未访问过 vis[e[i].ver]=true; //标记这个未访问过的右边点 if(!match[e[i].ver]||dfs(match[e[i].ver])) //如果空闲 或 原匹配的点可以让位 { match[e[i].ver]=x; return true; } //左节点x可以占用这个右节点y } return false; //无法找到匹配,即该情况下不会出现增广路 }
(12)基环树找环
vector<int> G[MAXN]; //基环树 int fa[MAXN]; //dfs时的父亲 int dfn[MAXN], idx; //访问的时间 int loop[MAXN], cnt; //环 void get_loop(int u) { dfn[u] = ++ idx; //记录dfn序 for (int i = 0; i < G[u].size(); i ++) { int v = G[u][i]; if(v == fa[u]) continue ; if(dfn[v]) { //找到了环 if(dfn[v] < dfn[u]) continue ; loop[++ cnt] = v; for ( ; v != u; v = fa[v]) loop[++ cnt] = fa[v]; } else fa[v] = u, get_loop(v); //继续递归 } }
(13)欧拉函数 单点值 / 线性筛法
int euler(int x){ int ans=x; for(int i=2;i*i<=x;i++) if(x%i==0){ ans=ans/i*(i-1); while(x%i==0) x/=i; } if(x>1) ans=ans/x*(x-1); return ans; //返回ϕ(x)的值 } // ϕ(n) = n∗∏(i=1~k)((ai-1)/ai); -------------------------------------------------------- int phi[MAXN],vis[MAXN],prime[MAXN],tot=0; void GetPhi(int n){ //【phi线性筛法】 phi[1]=1; //特例:ϕ(1)=1; for(int i=2;i<=n;i++){ if(!vis[i]) prime[++tot]=i,phi[i]=i-1; //i是素数(1) for(int j=1;j<=tot&&i*prime[j]<=n;j++){ //枚举“i*质数” vis[i*prime[j]]=1; //标记“i*质数”为合数 if(i%prime[j]==0) //(3)注意:剪枝,赋值后直接break; {phi[i*prime[j]]=phi[i]*prime[j];break;} else phi[i*prime[j]]=phi[i]*(prime[j]-1); } //(2) } for(int i=1;i<=n;i++) cout<<phi[i]<<endl; }
(14)凸包
struct point{ double x,y; }a[10019]; int sta[10019],top,n; double ans=0.0; double cross(point p0,point p1,point p2) //计算向量叉积 { return (p1.x-p0.x)*(p2.y-p0.y)-(p1.y-p0.y)*(p2.x-p0.x); } double dis(point p1,point p2) //计算点p1p2的距离 { return sqrt((double)(p2.x-p1.x)*(p2.x-p1.x)+(p2.y-p1.y)*(p2.y-p1.y)); } bool cmp(point p1,point p2){ //进行极角排序 double tmp=cross(a[0],p1,p2); if(tmp>0) return true; else if(tmp==0&&dis(a[0],p1)<dis(a[0],p2)) return true; else return false; //↑↑若角度相同,则距离小的在前面 } void init(){ //输入,并把最左下方的点放在a[0],进行极角排序。 point p0; scanf("%lf%lf",&a[0].x,&a[0].y); p0.x=a[0].x; p0.y=a[0].y; int k=0; for(int i=1;i<n;i++){ scanf("%lf%lf",&a[i].x,&a[i].y); if( (p0.y>a[i].y) || ((p0.y==a[i].y)&&(p0.x>a[i].x)) ) p0.x=a[i].x,p0.y=a[i].y,k=i; //寻找左下角的点 } a[k]=a[0],a[0]=p0; //把原来0位置的点放到k位置(互换位置) sort(a+1,a+n,cmp); //除去0号点,其余n-1个点进行极角排序 } void graham(){ //极角排序法求凸包 if(n==1) top=0,sta[0]=0; if(n==2) top=1,sta[0]=0,sta[1]=1; if(n>2){ top=1,sta[0]=0,sta[1]=1; for(int i=2;i<n;i++){ while(top>0&&cross(a[sta[top-1]],a[sta[top]],a[i])<=0) top--; top++; sta[top]=i; } //每加入新的点,判断要出栈的凹点,并将新点入栈 } } int main(){ scanf("%d",&n); init(); graham(); //输入+极角排序+求凸包 for(int i=0;i<top;i++) ans+=dis(a[sta[i]],a[sta[i+1]]); ans+=dis(a[sta[0]],a[sta[top]]); printf("%.2lf ",ans); //凸包总周长 }
【一、搜索】
(1)dfs常见思路
1.确定dfs的边界(或剪枝) 2.记忆化搜索(或剪枝)
3.枚举方向(判断超界) 4.回溯(所有状态完全回溯)
vis[xx][yy]=true; dfs(xx,yy,...); vis[xx][yy]=false;
(2)树上dfs
------可用于 lca的pre_dfs 和 各种各样的树形dp 和 树链剖分
void pre_dfs(int u,int fa_){ for(int i=head[u];i;i=e[i].nextt){ int v=e[i].ver; //找到下一条相连的边 if(v==fa_) continue; dep[v]=dep[u]+1; //深度 dist[v]=dist[u]+e[i].w; //距离 fa[v]=u; pre_dfs(v,u); //记录father,递归 } }
(3)bfs常见思路
1.起点入队,并标记访问(可能不止一个) 2.队首元素向外扩展:head++ 。
3.枚举方向,判断超界及可行性,标记访问,答案累加,节点入队:tail++ 。
void bfs(int sx,int sy){ //BFS确定连通块 node now1; now1.x=sx,now1.y=sy,q.push(now1); vis[sx][sy]=1,flag[sx][sy]=tot,num[tot]++; maps[tot][num[tot]]=now1; //记录每个连通块中每个点的坐标 while(!q.empty()){ //进行BFS node now=q.front(),now1;q.pop(); for(int i=0;i<4;i++){ //上、下、左、右 int xx=now.x+dx[i],yy=now.y+dy[i]; if(!in_(xx,yy)||vis[xx][yy]||ss[xx][yy]!='X') continue; now1.x=xx,now1.y=yy,q.push(now1), vis[xx][yy]=1,flag[xx][yy]=tot; num[tot]++,maps[tot][num[tot]]=now1; //进队并记录信息 } } } ---------洛谷【p3070】岛游记
(4)二分常见思路
1.用于最小化最大值/最大化最小值。 2.设定l、r、mid,进行二分。
3.设置checks函数,判断是否可行。 4.更新ans,缩小区间l、r。
- 整数二分、实数二分
while(l<=r){ int mid=(l+r)>>1; if(check(mid)) ans=mid,r=mid-1; else l=mid+1; }
while(r-l>1e-8){ mid=(l+r)/2.0; if(checks(mid)) l=mid; else r=mid; }
(5)二分图染色 / 判定
bool dfs(int v,int c){ color[v]=c; //把该点染成颜色c(1或-1) for(int i=0;i<G[v].size();i++){ if(color[G[v][i]]==c) return false; //当前点与相邻点同色 if(color[G[v][i]]==0&&!dfs(G[v][i],-c)) return false; //如果当前点的邻点还没被染色,就染成-c } return true; //连通的点全部完成染色 } void solve(){ for(int i=0;i<V;i++) if(color[i]==0) if(!dfs(i,1)) { cout<<"no"<<endl; return; } cout<<"yes"<<endl; }
(6)二分图匹配
<1>最大匹配
- 匹配:“任意两条边没有公共端点”的边的集合。
- 最大匹配:边数最多的“匹配”;完美匹配:两侧节点一一对应的匹配。
- 最大点独立集:两边点数相同时,左边节点的个数n-最大匹配边数。
main函数中的循环(每次清空vis数组):
for(int i=1;i<=n;i++) //加入左侧每个节点,判断是否存在增广路 memset(vis,false,sizeof(vis)),ans+=dfs(i); //计算最大匹配边数
dfs寻找最大匹配(bool类型,维护match数组):
bool dfs(int x){ for(int i=head[x];i;i=e[i].nextt) //寻找连边 if(!vis[e[i].ver]){ //当前右节点在新左节点的匹配中未访问过 vis[e[i].ver]=true; //标记这个未访问过的右边点 if(!match[e[i].ver]||dfs(match[e[i].ver])) //如果空闲 或 原匹配的点可以让位 { match[e[i].ver]=x; return true; } //左节点x可以占用这个右节点y } return false; //无法找到匹配,即该情况下不会出现增广路 }
<2>最小链覆盖与反链
- 反链:一个点集,其中任意两个点都不在同一条链上。
- 覆盖:所有点都能分布在链上时,需要的最小链数。
【最小链覆盖数 = 最长(反链)长度】【最长链长度 = 最小(反链)覆盖数】
-------> 所以求反链可以转化为:求 最小链覆盖数 或 最长链长度。
【求最小链覆盖(最长反链)】二分图求最大匹配。
相当于把每个点拆成两个点,求最大点独立集的大小。
两边点数相同时,最大点独立集大小=左边点数n-最大匹配数。
【输出最小链覆盖的方案】整体思路是考虑合并原来拆开的两个点。
用vis数组来标记被右边的某个点匹配上了的左边点。
那么在左边却没有匹配上的点,肯定是某条链的端点(这个点最多只有一条边在链上)。
dfs每个在左边并且没有匹配上的点 i,找它在右边的对应端点 i(合并拆成的两个点)。
寻找右边的 i 有没有匹配(找链的连向...),dfs,直到右边的某个 x 没有匹配,
那么就说明到了此链的另一个端点。过程中输出选点情况即可。
void dfs2(int now){ //最小链覆盖的方案 if(!match[now]){ printf("%d ",now); return; } dfs2(match[now]); printf("%d ",now); //↓↓即最小链覆盖的方案 } //相当于将一开始分开的两个点合并起来,按照匹配路径,寻找每条链的链长
(7)归并排序-逆序对模板
int a[maxn],ranks[maxn],ans=0; //ans记录逆序对的数量 void Merge(int l,int r){ //归并排序 if(l==r) return; int mid=(l+r)/2; //分治思想 Merge(l,mid); Merge(mid+1,r); //递归实现 int i=l,j=mid+1,k=l; while(i<=mid&&j<=r){ if(a[i]>a[j]){ ranks[k++]=a[j++]; ans+=mid-i+1; //逆序对的个数 } else ranks[k++]=a[i++]; } while(i<=mid) ranks[k++]=a[i++]; while(j<=r) ranks[k++]=a[j++]; for(int i=l;i<=r;i++) a[i]=ranks[i]; //排序数组传入原a数组中 }
(8)离散化模板
int kt[N],a[N]; //辅助数组kt[] int main(){ for(int i=1;i<=n;i++) cin>>a[i],kt[i]=a[i]; sort(kt+1,kt+n+1); //辅助数组进行排序 m=unique(kt+1,kt+n+1)-kt-1; //注意要-kt-1 for(int i=1;i<=n;i++) //↓↓第一个大于等于a[i]的位置 a[i]=lower_bound(kt+1,kt+m+1,a[i])-kt; //注意只用-kt }
【二、字符串】
https://www.cnblogs.com/FloraLOVERyuuji/p/10574154.html
(1)字符串哈希
H(C)=(c1*b^(m-1)+c2*b^(m-2)+....+cm*b^0) mod h。
- b为基数(base),H(C)的处理相当于把字符串看成b进制数。
预处理的过程通过递归计算:H(C,k)=H(C,k-1)*b+ck。
判断某段字符与另一匹配串是否匹配,即判断:
(↑↑某段字符:从位置k+1开始的长度为n的子串C’=ck+1 ck+2 .... ck+n;)
H(C’) =H(C,k+n)-H(C,k)*b^n 与 H(S) 的关系。
判断回文:正反hash。反hash要倒序预处理,注意左右边界。
ull自然溢出:powers数组设成ull类型,超出ull时会自然溢出(省时)。
哈希散列表:取余法,用链表记录每个hash值所在的位置(即对应的余数)。
(2)KMP模式匹配
题目:给你两个字符串,寻找其中一个字符串是否包含另一个字符串。
<1>原短字符串a的【自我匹配】
void pre(){ //【预处理nextt[i]】 nextt[1]=0; int j=0; //j指针初始化为0 for(int i=1;i<m;i++){ //a数组自我匹配,从i+1=2与1比较开始 while(j>0&&a[i+1]!=a[j+1]) j=nextt[j]; //↑自身无法继续匹配且j还没减到0,考虑返回匹配的剩余状态 if(a[i+1]==a[j+1]) j++; //这一位匹配成功 nextt[i+1]=j; //记录这一位向前的最长匹配 } }
<2>【原串a与询问串b】的匹配
- 在b串中寻找a串出现的位置:
void kmp(){ //在b串中寻找a串出现的位置 int ans=0,j=0; for(int i=0;i<n;i++){ //扫描b,寻找a的匹配 while(b[i+1]!=a[j+1]&&j>0) j=nextt[j]; //↑不能继续匹配且j还没减到0(之前的匹配有剩余状态) if(b[i+1]==a[j+1]) j++; //匹配加长,j++ if(j==m){ //【一定要把这个判断写在j++的后面!】 printf("%d ",i+1-m+1); //子串a的起点在母串b中的位置 j=nextt[j]; //继续寻找匹配 } //【↑↑巧妙↑↑这里不用返回0,只用返回上一匹配值】 } //注意:如果询问串的不重叠出现次数,则j必须变成0 }
- 求b串与a串匹配的最大长度:
int kmp(){ int j=0; //求f[i]数组 for(int i=0;i<n;i++){ //扫描长串b while(( j==m || b[i+1]!=a[j+1] ) && j>0) j=nextt[j]; //↑不能继续匹配且j还没减到0(之前的匹配有剩余状态)或 a在b中找到完全匹配 if(b[i+1]==a[j+1]) j++; //匹配加长,j++ f[i+1]=j; //此位置及之前与原串组成的最长匹配 // (if(f[i+1]==m),此时a在b中找到完全匹配) }
【拓展】循环同构串的最小表示法 (kmp思想)
(3)Trie字典树
Trie树:一种用于实现字符串快速检索的多叉树结构。
bool tail[SIZE]; //标记串尾元素 int trie[SIZE][26],tot=1; //SIZE:字符串最大长度(层数) //tot为节点编号,用它可以在trie数组中表示某层的某字母是否存在 void insert(char* ss){ //插入一个字符串 int len=strlen(ss),p=1; //p初始化为根节点1 for(int k=0;k<len;k++){ int ch=ss[k]-'a'; //小写字符组成串的某个字符,变成数字 if(trie[p][ch]==0) trie[p][ch]=++tot; //trie存编号tot //↑↑↑不存在此层的这个字符,新建结点,转移边 p=trie[p][ch]; //指针移动,连接下一个位置 } tail[p]=true; //s中字符扫描完毕,tail标记字符串的末位字符(的编号p) } bool searchs(char* ss){ //检索字符串是否存在 int len=strlen(ss),p=1; //p初始化为根节点 for(int k=0;k<len;k++){ p=trie[p][ss[k]-'a']; //寻找下一处字符 if(p==0) return false; //某层字符没有编号,不存在,即串也不存在 } return tail[p]; //判断最后一个字符所在的位置是否是某单词的末尾 }
- 难题:【bzoj4260】按位异或(trie树维护异或前缀和)
- 难题:【p3065】第一(拓扑排序+trie树)
(4)AC自动机
//统计在文本串中出现次数最多的单词。 int n,cnt=0,q[100019]; string tmp[100019]; struct node{ int fail,end,ch[30]; }trie[100019]; struct Result{ int num,pos; }ans[100019]; //所有单词的出现次数 bool operator <(Result a,Result b){ if(a.num!=b.num) return a.num>b.num; else return a.pos<b.pos; } void make_trie(string tmp,int id){ //Trie树 int len=tmp.length(),p=0; for(int i=0;i<len;i++){ int s=tmp[i]-'a'; if(!trie[p].ch[s]) trie[p].ch[s]=++cnt; p=trie[p].ch[s]; } trie[p].end=id; } void get_fail(){ int l=0,r=0; //广搜head、tail指针 for(int i=0;i<26;i++) if(trie[0].ch[i]!=0) trie[trie[0].ch[i]].fail=0,q[++r]=trie[0].ch[i]; while(l<r){ int p=q[++l]; for(int i=0;i<26;i++){ int v=trie[p].ch[i]; if(v) trie[v].fail=trie[trie[p].fail].ch[i],q[++r]=v; //记录fail指针 else trie[p].ch[i]=trie[trie[p].fail].ch[i]; //“虚指针” } } } void AC(string tmp){ //文本串匹配 int len=tmp.length(),p=0; for(int i=0;i<len;i++){ p=trie[p].ch[tmp[i]-'a']; int v=p; while(v) ans[trie[v].end].num++,v=trie[v].fail; } } int main(){ while(519){ cin>>n; if(n==0) break; cnt=0; memset(trie,0,sizeof(trie)); for(int i=1;i<=n;i++){ cin>>tmp[i]; ans[i].num=0,ans[i].pos=i,make_trie(tmp[i],i); } trie[0].fail=0; get_fail(); cin>>tmp[0]; AC(tmp[0]); sort(&ans[1],&ans[n+1]); //按出现次数从大到小排序 cout<<ans[1].num<<endl<<tmp[ans[1].pos]<<endl; for(int i=2;i<=n;i++){ //出现次数相同的所有单词 if(ans[i].num!=ans[i-1].num) break; cout<<tmp[ans[i].pos]<<endl; } } }
(5)Manacher算法
void Manacher(){ //求最长回文子串的长度 t[0]='$',t[1]='#'; //【1】加入'#' for(int i=0;i<n;i++) t[i*2+2]=ss[i],t[i*2+3]='#'; n=n*2+2,t[n]='%'; //更新字符串长度 int last_max=0,last_id=0; //【2】求出p[]数组 for(int i=1;i<n;i++){ //↓↓继承i关于id的对称点j的最长匹配长度 p[i]=(last_max>i)?min(p[2*last_id-i],last_max-i):1; while(t[i+p[i]]==t[i-p[i]]) p[i]++; //然后p[i]自身进行拓展 if(last_max<i+p[i]) last_max=i+p[i],last_id=i; //更新mx和id ans_Len=max(ans_Len,p[i]-1); //最长回文子串的长度 } }
(6)后缀数组
https://www.cnblogs.com/FloraLOVERyuuji/p/10382408.html
const int maxn=500019; int n,m; char s[maxn]; int rank[maxn],b[maxn],sa[maxn],tp[maxn],tax[maxn],height[maxn]; void qsort(){ //(1)基数排序 for(int i=0;i<=m;i++) tax[i]=0; for(int i=1;i<=n;i++) tax[rank[i]]++; for(int i=1;i<=m;i++) tax[i]+=tax[i-1]; for(int i=n;i>=1;i--) sa[tax[rank[tp[i]]]--]=tp[i]; } void SuffixSort(){ //(2)后缀排序 for(int i=1;i<=n;i++) rank[i]=s[i],tp[i]=i; m=519; qsort(); //第一次基数排序 for(int k=1;k<=n;k<<=1){ int p=0; for(int i=n-k+1;i<=n;i++) tp[++p]=i; //for(int i=1;i<=k;i++) tp[++p]=n-k+i; for(int i=1;i<=n;i++) if(sa[i]>k) tp[++p]=sa[i]-k; qsort(); // swap(rank,tp); for(int i=1;i<=n;i++) //注意此处数组交换的方式 b[i]=tp[i],tp[i]=rank[i],rank[i]=b[i]; rank[sa[1]]=p=1; for(int i=2;i<=n;i++) rank[sa[i]]=(tp[sa[i]]==tp[sa[i-1]] &&tp[sa[i]+k]==tp[sa[i-1]+k])?p:++p; if(p>=n) break; m=p; } } void getH(){ //(3)求height[] int k=0; for(int i=1;i<=n;i++){ if(k) k--; int j=sa[rank[i]-1]; while(s[i+k]==s[j+k]) k++; height[rank[i]]=k; } } //注意:此模板字符串从1位置开始,即scanf("%s",s+1);
【三、数据结构】
(1)树状数组
<1> 单点修改,区间查询:ans=query(y)-query(x-1)。
void add(ll x,ll k) //单点修改、维护前缀和 { for(i=x;i<=n;i+=i&-i) c[i]+=k; } ll query(ll x) //区间查询、查询前缀和 { ll sum=0; for(i=x;i>0;i-=i&-i) sum+=c[i]; return sum; }
<2> 区间修改,单点查询:c[x]被设置为差分数组前缀和,初始化为0。
区间修改:add(x,k),add(y+1,-k); 单点查询:ans=a[x]+query(x);
<3> 区间修改,区间查询:维护两个数组的前缀和。
sum1[i]=d[i]; sum2[i]=d[i]∗i; (d是差分数组)
直接把a数组处理成前缀和的形式(省略sum数组):
scanf("%lld",&a[i]),a[i]+=a[i-1];
区间修改:add(x,k),add(y+1,-k);
区间查询:query(y)-query(x-1)+a[y]-a[x-1];
每次用【差分】思路修改时:sum1[x]+k,sum1[y+1]-k ; sum2[x]+x*k,sum2[y+1]-(y+1)*k。
void add(ll x,ll k) //维护(差分数组的)区间前缀和 { for(int i=x;i<=n;i+=i&-i) sum1[i]+=k,sum2[i]+=x*k; }
查询位置x的差分前缀和即:(x+1)*sum1数组中p的前缀和-sum2数组中p的前缀和。
ll query(ll x) //查询(差分数组的)区间前缀和 { ll sum=0; for(int i=x;i>0;i-=i&-i) sum+=(x+1)*sum1[i]-sum2[i]; return sum; }
<4> 二维 —— 单点修改,区间查询
void add(ll x,ll y,ll k){ //【单点修改】 for(int i=x;i<=n;i+=i&-i) for(int j=y;j<=m;j+=j&-j) c[i][j]+=k; } //【维护二维前缀和】 ll query(ll x,ll y){ //【查询二维前缀和】 ll sum=0; //即:从左上角的(1,1)到(x,y)的矩阵和 for(int i=x;i>=1;i-=i&-i) for(int j=y;j>=1;j-=j&-j) sum+=c[i][j]; return sum; //返回二维前缀和 }
<5> 二维 —— 区间修改,单点查询
修改时:add(x,y,k),add(xx+1,yy+1,k),add(xx+1,y,-k),add(x,yy+1,-k);
修改时用到了差分的思想,查询时直接 a[x][y]+query(x,y) 即可。
(2)单调队列
1、维护队首可行性,head++;
2、维护队尾单调性,并插入当前元素;
3、取出队头的最优解,进行DP转移。
int head=1,tail=1; //【滑动窗口·区间max】 q[1].x=a[1]; q[1].id=1; //初始点为1 for(int i=2;i<=n;i++){ //从2开始循环 while(head<=tail && q[head].id<i-m+1) head++; //id的作用:判断区间长度 if(i>=m) printf("%d ",q[head].x); //每一次的队头都是当前段最大值 while(head<=tail && q[tail].x<=a[i]) tail--; //↑↑新数比前几个大,前几个不可能再成为最大值(可能不止一个) q[++tail].x=a[i]; q[tail].id=i; //a[i]加入队尾 //即:维护一个单调递减队列,如果后方有数更大,前面就全部删除。 }
(3)线段树
struct SegmentTree{ int l,r,sum; }tree[4*N]; void PushUp(int rt){ tree[rt].sum=tree[rt<<1].sum+tree[rt<<1|1].sum; } void build(int l,int r,int rt){ //【建树】 tree[rt].l=l; tree[rt].r=r; //建立标号与区间的关系 if(l==r){ scanf("%d",&tree[rt].sum); return; } //叶子节点 int mid=(l+r)/2; build(l,mid,rt<<1),build(mid+1,r,rt<<1|1); PushUp(rt); //将修改值向上传递 } void add(int p,int rt){ //【单点修改】 if(tree[rt].l==tree[rt].r){ tree[rt].sum+=y; return; } //叶子节点 int mid=(tree[rt].l+tree[rt].r)>>1; if(p<=mid) add(p,rt<<1); else add(p,rt<<1|1); tree[rt].sum=tree[rt<<1].sum+tree[rt<<1|1].sum; //pushup } void query(int p,int rt){ //【单点查询】 if(tree[rt].l==tree[rt].r){ ans=tree[rt].sum; return; } //叶子节点 int mid=(tree[rt].l+tree[rt].r)>>1; if(p<=mid) query(p,rt<<1); else query(p,rt<<1|1); } void sum(int rt){ //【区间查询求和】 if(tree[rt].l>=x&&tree[rt].r<=y) //区间完全包含 { ans+=tree[rt].sum; return; } int mid=(tree[rt].l+tree[rt].r)>>1; if(x<=mid) sum(rt<<1); //区间部分重叠,递归左右 if(y>=mid+1) sum(rt<<1|1); }
(4)点分治
主程序中: root=0; sum=f[0]=n; //一开始,root初始化为0,用于找重心
getroot(1,0); solve(root); //从重心开始点分治
int n,m,k,head[N],cnt; //head[]和cnt(=tot) int root,sum; //当前查询的根,当前递归的这棵树的大小 int vis[N]; //某一个点是否被当做根过 int sz[N]; //每个点下面子树的大小 int f[N]; //每个点为根时,最大子树大小 int dep[N]; //每个点的深度(此时是与根节点的距离) int o[N]; //每个点的深度(继承dep[],用于排序,进而用于二分) int ans; //最终统计的答案 void getroot(int u,int fa){ //dfs求【重心】和【子树大小】 sz[u]=1; f[u]=0; for(int i=head[u];i;i=e[i].nextt){ int v=e[i].ver; if(v==fa||vis[v]) continue; getroot(v,u); sz[u]+=sz[v]; f[u]=max(f[u],sz[v]); } f[u]=max(f[u],sum-sz[u]); //注意:可能是另外一半的树 if(f[u]<f[root]) root=u; //更新重心 } void getdeep(int u,int fa){ //dfs求出与根节点的【距离dep】 o[++cnt]=dep[u]; //用于排序 for(int i=head[u];i;i=e[i].nextt){ int v=e[i].ver; if(v==fa||vis[v]) continue; dep[v]=dep[u]+e[i].w; getdeep(v,u); } } int calc(int u,int d0){ //↑↑↑此时以u为根节点,统计子树中符合条件的点对个数 cnt=0; dep[u]=d0; getdeep(u,0); sort(o+1,o+cnt+1); //排序,便于二分 int l=1,r=cnt,res=0; while(l<r){ if(o[l]+o[r]<=k) res+=r-l,l++; else r--; //二分求符合条件的点对个数 } return res; } void solve(int u){ ans+=calc(u,0); vis[u]=1; //↑↑会产生非法路径(被u的某个子树完全包含,路径不能合并) for(int i=head[u];i;i=e[i].nextt){ //递归子树 int v=e[i].ver; if(vis[v]) continue; //fa ans-=calc(v,e[i].w); //容斥原理去除非法答案 //↑↑在处理子树时,将初始长度设为连接边长e[i].w; //这样做就相当于给子树的每个组合都加上了u—>..的路径。 sum=sz[v]; root=0; //重设当前总树大小,寻找新的分治点 getroot(v,0); solve(root); //递归新的分治点(重心) } }
(5)分块&莫队
https://www.cnblogs.com/FloraLOVERyuuji/p/10428197.html
while(l>q[i].l) add(col[--l]); while(r<q[i].r) add(col[++r]); while(l<q[i].l) del(col[l++]); while(r>q[i].r) del(col[r--]); //↑↑q[i].l/r即正在处理的询问区间的两个端点
int block=(int)sqrt(n); //分块 for(int i=1;i<=n;i++) pos[i]=(i-1)/block+1; for(int i=1;i<=m;i++) reads(q[i].l),reads(q[i].r),q[i].id=i; sort(q+1,q+m+1,cmp); solve(); //离线,进行莫队算法 sort(q+1,q+m+1,cmp_id); //复原询问的编号
(6)树链剖分 / 平衡树 / 左偏树(可并堆) / 主席树 见上面
【四、图论】
(1)Tarjan缩点
int dfn[N],low[N],stack[N],vis[N]; int dfn_=0,top_=0,sum=0,col[N]; //dfn序,栈中位置top,强连通个数sum,每点所属连通块编号col[i] for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i); void tarjan(int u){ //dfn_记录当前dfs序到达的数字 dfn[u]=low[u]=++dfn_,vis[u]=1,stack[++top_]=u; //步骤一:初始化 for(int i=head[u];i;i=e[i].nextt){ //步骤二:枚举连向点,递归更新 if(!dfn[e[i].ver]) tarjan(e[i].ver),low[u]=min(low[u],low[e[i].ver]); else if(vis[e[i].ver]) low[u]=min(low[u],dfn[e[i].ver]); //这里写dfn或low都可以 } //↑↑步骤三:已经到达过,判断是否在当前栈内(栈内都是当前情况下能相连的点) if(dfn[u]==low[u]){ col[u]=++sum; vis[u]=0; while(stack[top_]!=u){ //u上方的节点是可以保留的 col[stack[top_]]=sum; vis[stack[top_]]=0,top_--; } top_--; //col数组记录每个点所在连通块的编号 } } int times[N],du[N]; //times数组/du数组记录每个强连通分量的大小/入度 for(int u=1;u<=n;u++){ for(int i=head[u];i;i=e[i].nextt) if(col[e[i].ver]!=col[u]) du[col[e[i].ver]]++; times[col[u]]++; //记录强连通分量大小 }
(2)拓扑排序
queue<int>q; //给出n个顺序关系,问是否合法。 bool tp_sort(){ //拓扑排序判环 for(int i=1;i<=n;i++) if(rd[i]==0) q.push(i); while(!q.empty()){ x=q.front(),q.pop(),cnt++; for(int i=head[x];i;i=e[i].nextt){ rd[e[i].ver]--; //rd--,相当于‘删边’ if(rd[e[i].ver]==0) q.push(e[i].ver); } } if(cnt==n) return true; return false; } //拓扑判环
(3)差分约束
- 给出一些形如x-y<=b不等式的约束,问你满足条件是否有解。
方法:找适当的方式建边(一般是在x,y之间建立长度为b的边),转换成最短路问题。
建边:1.b-a<=-c,w(b,a)=-c; 2.w(a,b)=c; 3.w(a,b)=w(b,a)=0。
因为随便值为多少,所以从0向1~n每个点连边w[0,i]=0。用SPFA求最短路,出现环则No。
(4)2-sat 问题
- 对于每个要求(a∨b),转换为 ( ¬a→b )∧(¬b→a ) ,
- 即:「若 a 假则 b 必真,若 b 假则 a 必真」。
- 然后按照箭头的方向建有向边,进行相关的缩点、拓扑排序操作。
#include<cmath> #include<cstdio> #include<cstring> #include<cassert> #include<iostream> #include<algorithm> #include<queue> #include<vector> #include<map> #include<set> #include<deque> using namespace std; typedef long long ll; #define R register /*【p4782】2-sat问题 有n个布尔变量x1~xn,另有m个需要满足的条件, 每个条件的形式都是“xi为true/false或xj为true/false”。 2-SAT问题的目标是给每个变量赋值使得所有条件得到满足。*/ void reads(int &x){ //读入优化(正负整数) int f=1;x=0;char s=getchar(); while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();} while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();} x*=f; //正负号 } const int N=2500019,M=5000019; int n,m,dfn[N],low[N],stack[N],vis[N]; int dfn_=0,top_=0,colnum=0,col[N];//siz[N]; int head[N],tot=0,rd[N],okk=1; struct node{ int ver,nextt; }e[M]; inline void add(R int x,R int y){ e[++tot].nextt=head[x],e[tot].ver=y,head[x]=tot; } inline void tarjan(int x){ dfn[x]=low[x]=++dfn_,stack[++top_]=x,vis[x]=1; for(R int i=head[x];i;i=e[i].nextt){ if(!dfn[e[i].ver]) tarjan(e[i].ver),low[x]=min(low[x],low[e[i].ver]); else if(vis[e[i].ver]) low[x]=min(low[x],dfn[e[i].ver]); } if(dfn[x]==low[x]){ col[x]=++colnum; vis[x]=0; //siz[colnum]++; while(stack[top_]!=x){ //x上方的节点是可以保留的 col[stack[top_]]=colnum; //siz[colnum]++; vis[stack[top_]]=0,top_--; } top_--; //col数组记录每个点所在连通块的编号 } } int main(){ int u,v,uw,vw; reads(n),reads(m); for(R int i=1;i<=m;i++){ reads(u),reads(uw),reads(v),reads(vw); int notu=uw^1,notv=vw^1; //命题的否定 add(u+notu*n,v+vw*n),add(v+notv*n,u+uw*n);//(非u,v),(非v,u) } for(R int i=1;i<=n*2;i++) if(!dfn[i]) tarjan(i); for(R int i=1;i<=n;i++) if(col[i]==col[i+n]){ okk=0; break; } if(okk){ printf("POSSIBLE "); //↓↓此情况下的真假 for(int i=1;i<=n;i++) printf("%d ",col[i]>col[i+n]); } else printf("IMPOSSIBLE "); return 0; }
(5)割点
/*【p3225】矿场搭建 */ //【标签】数学统计 + 分情况讨论 + 割点 + 无向图双连通分量 /* 割点:在一个【无向图】中,如果有一个顶点集合, 删除这个顶点集合以及这个集合中所有顶点相关联的边以后, 图的连通分量增多,就称这个点集为割点【集合】。 */ // 双连通分量:无向图中的强连通分量,去掉任意一个节点都不会改变连通性 /*【tarjan算法求割点】 1.判断根节点:计算其子树数量,如果有2棵即以上的子树,就是割点。 2.回顾一下low[i]的定义:u及其子树中的点,能够连向到的dfn最小的点的dfn值。 2.对于边(u,v),如果low[v]>=dfn[u],那么low[v]没有返祖边,此时u就是割点。*/ /*【思路】统计每个双连通分量里的割点的个数,并分情况讨论。 1.若该连通分量里割点>=2,可以通过其他割点跑到其他的双连通分量里。 2.若该连通分量里割点=1,所以要在任意一个非割点处建一个出口。 3.若该连通分量里割点=0,说明它与外界完全不连通,需要建两个出口以防万一。*/ const int N=519; int n,m,tot=0,head[N],dfn[N],low[N],cut[N],vis[N],root; int dfn_=0,sum,cnt,cut_num; //连通块数sum,当前连通块节点数、割点数 int kase=0; ll ans1,ans2; //选点数,方案数 struct node{ int ver,nextt; }e[N*2]; void add(int u,int v){ e[++tot].ver=v,e[tot].nextt=head[u],head[u]=tot; } void tarjan(int u,int fa){ //【tarjan求割点】 dfn[u]=low[u]=++dfn_; int child=0; //fa是子树的根节点,如果儿子数>=2则是割点 for(int i=head[u];i;i=e[i].nextt){ if(!dfn[e[i].ver]){ tarjan(e[i].ver,u),low[u]=min(low[u],low[e[i].ver]); if(low[e[i].ver]>=dfn[u]&&u!=root) cut[u]=true; if(u==root) child++; //root节点的儿子数++ } low[u]=min(low[u],dfn[e[i].ver]); } if(child>=2&&u==root) cut[u]=true; } void dfs(int u){ //遍历每个连通块 vis[u]=sum; cnt++; //节点数cnt++ for(int i=head[u];i;i=e[i].nextt){ if(cut[e[i].ver]&&vis[e[i].ver]!=sum) cut_num++,vis[e[i].ver]=sum; //e[i].ver是割点 if(!vis[e[i].ver]) dfs(e[i].ver); //非割点 } } void init(){ memset(head,0,sizeof(head)); memset(dfn,0,sizeof(dfn)); memset(low,0,sizeof(low)); memset(cut,0,sizeof(cut)); memset(vis,0,sizeof(vis)); dfn_=n=tot=sum=ans1=0,ans2=1; } int main(/*hs_love_wjy*/){ while(scanf("%d",&m)==1&&m){ init(); //日常清零 for(int i=1,u,v;i<=m;i++){ reads(u),reads(v),n=max(n,max(u,v)); add(u,v),add(v,u); //↑↑记录点的总个数n } for(int i=1;i<=n;i++) if(!dfn[i]) root=i,tarjan(i,0); for(int i=1;i<=n;i++) if(!vis[i]&&!cut[i]){ sum++; cnt=cut_num=0; dfs(i); //到达一个新的连通块 if(!cut_num) ans1+=2,ans2*=cnt*(cnt-1)/2; //建两个 if(cut_num==1) ans1++,ans2*=cnt; //建一个 } printf("Case %d: %lld %lld ",++kase,ans1,ans2); } }
【五、网络流】
(1)最大流 / 最小割
网络流最小割 最小割 = 最大流 。注意拆点操作。
(有正负权值时)最大权闭合子图 = s连的正权值之和 - min_cut 。
int s,t,tot=1,n,m,ans,head[N],dep[N],cur[N]; //s为源点,t为汇点 struct node{ int nextt,ver,w; }e[M]; void add(int x,int y,int z) { e[++tot].ver=y,e[tot].nextt=head[x],e[tot].w=z,head[x]=tot; } bool bfs(){ memset(dep,0,sizeof(dep)); //dep记录深度 memcpy(cur,head,sizeof(head)); queue<int> q; while(!q.empty()) q.pop(); dep[s]=1; q.push(s); while(!q.empty()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=e[i].nextt) if((e[i].w>0)&&(dep[e[i].ver]==0)) //分层 dep[e[i].ver]=dep[u]+1,q.push(e[i].ver); } if(dep[t]!=0) return 1; else return 0; //此时不存在分层图也不存在增广路 } int dfs(int u,int lastt){ int cnt=0; if(u==t) return lastt; //lastt:此点还剩余的流量 for(int i=cur[u];i&&cnt<lastt;i=e[i].nextt){ cur[u]=i; //当前弧优化 if((dep[e[i].ver]==dep[u]+1)&&(e[i].w!=0)){ int f=dfs(e[i].ver,min(lastt-cnt,e[i].w)); if(f>0){ e[i].w-=f,e[i^1].w+=f; cnt+=f; } } } if(cnt<lastt) dep[u]=-1; return cnt; } void dinic(){ ans=0; while(bfs()) ans+=dfs(s,2e9); }
(2)最小费用最大流
struct edge{ ll ver,nextt,flow,cost; }e[2*N]; ll tot=-1,n,m,S,T,maxf=0,minc=0; ll flow[N],head[N],dist[N],inq[N],pre[N],lastt[N]; void add(ll a,ll b,ll f,ll c) { e[++tot].nextt=head[a],head[a]=tot, e[tot].ver=b,e[tot].flow=f,e[tot].cost=c; } bool spfa(ll S,ll T){ queue<ll> q; memset(inq,0,sizeof(inq)); memset(flow,0x7f,sizeof(flow)); memset(dist,0x7f,sizeof(dist)); q.push(S),dist[S]=0,pre[T]=-1,inq[S]=1; while(!q.empty()){ ll x=q.front(); q.pop(); inq[x]=0; for(ll i=head[x];i!=-1;i=e[i].nextt){ if(e[i].flow>0&&dist[e[i].ver]>dist[x]+e[i].cost){ dist[e[i].ver]=dist[x]+e[i].cost; pre[e[i].ver]=x,lastt[e[i].ver]=i; flow[e[i].ver]=min(flow[x],e[i].flow); if(!inq[e[i].ver]) q.push(e[i].ver),inq[e[i].ver]=1; } } } return pre[T]!=-1; } void mcmf(){ while(spfa(S,T)){ ll now=T; //↓↓最小费用最大流 maxf+=flow[T],minc+=dist[T]*flow[T]; while(now!=S){ //↓↓正边流量-,反边流量+ e[lastt[now]].flow-=flow[T]; e[lastt[now]^1].flow+=flow[T]; //↑↑利用xor1“成对储存”的性质 now=pre[now]; //维护前向边last,前向点pre } } } //注意:tot=1,memset(head,-1,sizeof(head));
【六、动态规划】
(1)树形DP
https://www.cnblogs.com/FloraLOVERyuuji/p/10560021.html
(2)基环树DP
https://www.cnblogs.com/FloraLOVERyuuji/p/10419674.html
(3)状压DP
https://www.cnblogs.com/FloraLOVERyuuji/p/10601568.html
(4)矩阵优化DP
https://www.cnblogs.com/FloraLOVERyuuji/p/10512899.html
(5)斜率优化DP
- 对于每个斜率方程 (Y(j2)-Y(j1))/(X(j2)-X(j1)):
1.将数据进行预处理(求sum等操作),优化序列。
2.写状态转移方程,如果是二维,要使用二维单调队列。
3.推导不等式,化成斜率的一般式,一般使用化除为乘。
4.从而得到X,Y的定义式,用double类型表示出来。
5.建立一个类似优先队列的斜率单调队列。
6.维护头尾可行性以及斜率单调性,队头为最优答案。
【七、数论】
(1)欧拉函数
https://www.cnblogs.com/FloraLOVERyuuji/p/10423022.html
int euler(int x){ int ans=x; for(int i=2;i*i<=x;i++) if(x%i==0){ ans=ans/i*(i-1); while(x%i==0) x/=i; } if(x>1) ans=ans/x*(x-1); return ans; //返回ϕ(x)的值 } // ϕ(n) = n∗∏(i=1~k)((ai-1)/ai);
int phi[MAXN],vis[MAXN],prime[MAXN],tot=0; void GetPhi(int n){ //【phi线性筛法】 phi[1]=1; //特例:ϕ(1)=1; for(int i=2;i<=n;i++){ if(!vis[i]) prime[++tot]=i,phi[i]=i-1; //i是素数(1) for(int j=1;j<=tot&&i*prime[j]<=n;j++){ //枚举“i*质数” vis[i*prime[j]]=1; //标记“i*质数”为合数 if(i%prime[j]==0) //(3)注意:剪枝,赋值后直接break; {phi[i*prime[j]]=phi[i]*prime[j];break;} else phi[i*prime[j]]=phi[i]*(prime[j]-1); } //(2) } for(int i=1;i<=n;i++) cout<<phi[i]<<endl; }
- 原根个数为φ(φ(m));由于素数的φ(m)=m-1,所以素数的原根=φ(m-1)。
(2)欧拉定理
欧拉定理:
扩展欧拉定理:
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<queue> #include<string> #include<cmath> #include<map> using namespace std; typedef long long ll; // p5091 【模板】 扩展欧拉定理 // 求a^b mod m //【扩展欧拉定理】 b≥φ(m)时,a^b≡a^((bmodφ(m))+φ(m)) mod m ; int a,b,m,ans=1; bool flag; int euler(int x){ int ans=x; for(int i=2;i*i<=x;i++) if(x%i==0){ ans=ans/i*(i-1); while(x%i==0) x/=i; } if(x>1) ans=ans/x*(x-1); return ans; //返回ϕ(x)的值 } int main(){ char c; scanf("%d%d",&a,&m); int phi=euler(m); while(!isdigit(c=getchar())); //边读入b边取模 for(;isdigit(c);c=getchar()){ b=b*10+c-'0'; if(b>=phi) flag=true,b%=phi; } if(flag) b+=phi; //只有b>=phi时,公式才成立(否则不用+φ(m)) for(int i=20;i>=0;i--){ //ksm求a^((bmodφ(m))+φ(m)) ans=1ll*ans*ans%m; if(b&(1<<i)) ans=1ll*ans*a%m; } cout<<ans<<endl; return 0; }
(3)卢卡斯定理
- 公式:Lucas(C(n,m),p)=Lucas(C(n%p,m%p),p)*Lucas(C(n/p,m/p),p)
long long inv[100010],kk[100010],p; long long lucas(int x,int y){ if(x<y) return 0; //无法构成组合数,返回答案为0 if(x<p) return kk[x]*inv[y]*inv[x-y]%p; //x的阶乘*(y!%p的逆元)*((x-y)!%p的逆元) else return lucas(x/p,y/p)*lucas(x%p,y%p)%p; } int main(){ int T,n,m; scanf("%d",&T); while(T--){ scanf("%d%d%lld",&n,&m,&p); inv[0]=inv[1]=kk[0]=kk[1]=1; //阶乘数组&&逆元数组初始化 for(int i=2;i<=n+m;i++) kk[i]=kk[i-1]*i%p; for(int i=2;i<=n+m;i++) inv[i]=(p-p/i)*inv[p%i]%p; for(int i=2;i<=n+m;i++) inv[i]=inv[i-1]*inv[i]%p; //逆元的阶乘 等于 k!%p的逆元 printf("%lld ",lucas(n+m,m)); //调用卢卡斯函数 } }
(4)莫比乌斯反演
https://www.cnblogs.com/FloraLOVERyuuji/p/10539217.html
void get_mu(int n){ mu[1]=1; for(int i=2;i<=n;i++){ if(!vis[i]) primes[++cnt]=i,mu[i]=-1; for(int j=1;j<=cnt&&primes[j]*i<=n;j++){ vis[primes[j]*i]=1; if(i%primes[j]==0) break; else mu[i*primes[j]]=-mu[i]; } } }
(5)线性基
// 最大异或和:给定n个整数(数字可能重复), // 在这些数中选取任意个,使得他们的异或和最大。 ll base[5019]; int main(){ ll n,tmp; scanf("%lld",&n); for(int i=1;i<=n;i++){ scanf("%lld",&tmp); for(int j=50;j>=0;--j) if(tmp&(1LL<<j)) { if(!base[j]) base[j]=tmp; tmp^=base[j]; } } ll ans=0; for(int i=50;i>=0;i--) if(ans<(ans^base[i])) ans^=base[i]; printf("%lld ",ans); return 0; }
(6)矩阵乘法
struct Mat{ ll m[9][9]; }a,dp; //a是原始矩阵,dp是构造的矩阵 Mat mat_mul(Mat x,Mat y){ //矩阵乘:x*y=c Mat c; memset(c.m,0,sizeof(c.m)); //【注意矩阵的‘清空’】 for(ll i=1;i<=3;i++) for(ll j=1;j<=3;j++) for(ll k=1;k<=3;k++) c.m[i][j]=(c.m[i][j]%mod+x.m[i][k]*y.m[k][j]%mod)%mod; return c; } Mat mat_pow(Mat x,ll y) //矩阵快速幂:ans*x^y { Mat ans; memset(ans.m,0,sizeof(ans.m)); //记得要随时清空 for(ll p=1;p<=3;p++) ans.m[p][p]=1; //初始化为单位矩阵【清空】 while(y){ if(y&1) ans=mat_mul(ans,x); x=mat_mul(x,x); y>>=1; } return ans; }
(7)Matrix-Tree定理
https://www.cnblogs.com/FloraLOVERyuuji/p/10524226.html
int Gauss(){ //高斯消元 int ans=1; for(int i=1;i<=tot;i++){ for(int j=i+1;j<=tot;j++) //tot是总点数 while(f[j][i]){ int t=f[i][i]/f[j][i]; for(int k=i;k<=tot;k++) f[i][k]=(f[i][k]-t*f[j][k]%mod+mod)%mod, swap(f[i][k],f[j][k]); ans=-ans; //辗转相除法 } ans=(ans*f[i][i])%mod; } return (ans+mod)%mod; //注意ans可能为负数 }
(8)FFT
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<string> #include<queue> #include<vector> #include<cmath> #include<map> using namespace std; typedef long long ll; //【p1919】A*B Problem void reads(int &x){ //读入优化(正负整数) int fa=1;x=0;char s=getchar(); while(s<'0'||s>'9'){if(s=='-')fa=-1;s=getchar();} while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();} x*=fa; //正负号 } const int N=1000019; struct complex{ //复数 double x,y; complex(){} //复数的相关运算 complex(double x,double y){this->x=x,this->y=y;} complex friend operator +(complex n1,complex n2) {return complex(n1.x+n2.x,n1.y+n2.y);} complex friend operator -(complex n1,complex n2) {return complex(n1.x-n2.x,n1.y-n2.y);} complex friend operator *(complex n1,complex n2) {return complex(n1.x*n2.x-n1.y*n2.y,n1.x*n2.y+n1.y*n2.x);} }a[N],b[N],tmpx,tmpy,wn,w; const double pi=3.1415926535897632; int n,m,turn[N],len=1,L=-1; char s1[N],s2[N]; int aa=0,bb=0,ans[N]; void FFT(complex *a,int typ){ for(int i=0;i<len;i++) if(i<turn[i]) swap(a[i],a[turn[i]]); for(int l=1;l<len;l<<=1){ wn=complex(cos(pi/l),typ*sin(pi/l)); for(int p=0;p<len;p+=(l<<1)){ w=complex(1,0); //a+b*i for(int i=p;i<p+l;i++,w=w*wn){ tmpx=a[i],tmpy=w*a[i+l]; a[i]=tmpx+tmpy,a[i+l]=tmpx-tmpy; } //↑↑用“蝴蝶操作”优化 } } } int main(){ //把每一位看成一个系数,最后再整合 reads(n); scanf("%s%s",s1,s2); for(int i=n-1;i>=0;i--) a[aa++].x=s1[i]-48; for(int i=n-1;i>=0;i--) b[bb++].x=s2[i]-48; while(len<(n+n)) len<<=1,L++; for(int i=0;i<=len;i++) turn[i]=(turn[i>>1]>>1)|((i&1)<<L); //↑↑位逆序替换,就找到了对应的turn位置 /* 实现思路:系数表示法—>点值表示法—>系数表示法。 后面的1表示要进行的变换是什么类型。 1表示从系数变为点值,-1表示从点值变为系数。 */ FFT(a,1),FFT(b,1); //从系数变为点值 for(int i=0;i<=len;i++) a[i]=a[i]*b[i]; //记录乘积答案 FFT(a,-1); //把乘积答案转化为各位置的系数 for(int i=0;i<=len;i++){ ans[i]+=(int)(a[i].x/len+0.5); //系数整合为大整数 if(ans[i]>=10) ans[i+1]+=ans[i]/10,ans[i]%=10, len+=(i==len); //判断是否要多一位 } while(!ans[len]&&len>=1) len--; //删除前导零 len++; while(--len>=0) cout<<ans[len]; //输出答案 }
【八、计算几何】
(二维凸包 / 旋转卡壳 / 半平面交)
struct point{ double x,y; }a[10019]; int sta[10019],top,n; double ans=0.0; double cross(point p0,point p1,point p2) //计算向量叉积 { return (p1.x-p0.x)*(p2.y-p0.y)-(p1.y-p0.y)*(p2.x-p0.x); } double dis(point p1,point p2) //计算点p1p2的距离 { return sqrt((double)(p2.x-p1.x)*(p2.x-p1.x)+(p2.y-p1.y)*(p2.y-p1.y)); } bool cmp(point p1,point p2){ //进行极角排序 double tmp=cross(a[0],p1,p2); if(tmp>0) return true; else if(tmp==0&&dis(a[0],p1)<dis(a[0],p2)) return true; else return false; //↑↑若角度相同,则距离小的在前面 } void init(){ //输入,并把最左下方的点放在a[0],进行极角排序。 point p0; scanf("%lf%lf",&a[0].x,&a[0].y); p0.x=a[0].x; p0.y=a[0].y; int k=0; for(int i=1;i<n;i++){ scanf("%lf%lf",&a[i].x,&a[i].y); if( (p0.y>a[i].y) || ((p0.y==a[i].y)&&(p0.x>a[i].x)) ) p0.x=a[i].x,p0.y=a[i].y,k=i; //寻找左下角的点 } a[k]=a[0],a[0]=p0; //把原来0位置的点放到k位置(互换位置) sort(a+1,a+n,cmp); //除去0号点,其余n-1个点进行极角排序 } void graham(){ //极角排序法求凸包 if(n==1) top=0,sta[0]=0; if(n==2) top=1,sta[0]=0,sta[1]=1; if(n>2){ top=1,sta[0]=0,sta[1]=1; for(int i=2;i<n;i++){ while(top>0&&cross(a[sta[top-1]],a[sta[top]],a[i])<=0) top--; top++; sta[top]=i; } //每加入新的点,判断要出栈的凹点,并将新点入栈 } } int main(){ scanf("%d",&n); init(); graham(); //输入+极角排序+求凸包 for(int i=0;i<top;i++) ans+=dis(a[sta[i]],a[sta[i+1]]); ans+=dis(a[sta[0]],a[sta[top]]); printf("%.2lf ",ans); //凸包总周长 }
——时间划过风的轨迹,那个少年,还在等你