zoukankan      html  css  js  c++  java
  • 【孤*执】#2019SX# 最后der省选模板总结 ——BY.hss

    部分参照这一篇   【浮*光】 #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; //标记每次下移一位,并清空上一位置的标记
    }
    比较复杂的push_down(【p2894】酒店)

    (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;
    }
    【p4782】2-sat问题

    (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;
    }
    p5091 【模板】 扩展欧拉定理 // 求a^b mod m

    (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]; //输出答案
    }
    【p1919】A*B Problem

    【八、计算几何】

    (二维凸包 / 旋转卡壳  / 半平面交)

    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); //凸包总周长
    }

                                                                        ——时间划过风的轨迹,那个少年,还在等你

  • 相关阅读:
    最近一月研报推荐次数最多的最热股票
    【2019年07月22日】A股最便宜的股票
    【07月19日】指数估值排名
    北上资金近1周流入排行榜
    主要股东近3年净买入排名
    【07月16日】A股滚动市净率PB历史新低排名
    【07月15日】A股滚动市盈率PE最低排名
    最近3年股息率最高排名
    主要股东近3年净买入排名
    【07月09日】预分红股息率最高排名
  • 原文地址:https://www.cnblogs.com/FloraLOVERyuuji/p/10638189.html
Copyright © 2011-2022 走看看