【孤*执】#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\n",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\n",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\n",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\n",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\n"); //↓↓此情况下的真假
        for(int i=1;i<=n;i++) printf("%d ",col[i]>col[i+n]); 
    } else printf("IMPOSSIBLE\n"); 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\n",++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\n",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\n",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\n",ans); //凸包总周长
}

 

 

 

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

 

posted @ 2019-04-04 19:53  花神&缘浅flora  阅读(303)  评论(0编辑  收藏  举报