1. 程式人生 > >【浮*光】 #noip總複習# hss_2018noip_rp++

【浮*光】 #noip總複習# hss_2018noip_rp++

【零. 序言】

------標頭檔案

#include<cstdio>
#include<iostream>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<set>
#include<vector>
#include<map>
#include<queue>
using namespace std;
typedef long long ll;

------讀入優化

void reads(int &x){
    int fx=1;x=0;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')fx=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    x=x*fx; //正負號
}

------並查集

int find_fa(int x){ return fa[x]=(fa[x]==x)?x:find_fa(fa[x]); }

------建邊

struct node{ int nextt,ver,w; }e[M*2]; int tot=0,head[N];

void add(int x,int y,int z){ e[++tot].nextt=head[x],e[tot].ver=y,e[tot].w=z,head[x]=tot; }

------dis函式

double dis(int i,int j){ /*注意結構體的使用*/
    return sqrt((double)(S[i].x-S[j].x)*(S[i].x-S[j].x)
        +(double)(S[i].y-S[j].y)*(S[i].y-S[j].y));
}

------快速冪

ll power(ll a,ll b,ll mod){
    ll anss=1; //注意初始化為1
    while(b>0){ //求a的b次方%mod
        if(b&1) anss=anss*a%mod;
        a=a*a%mod; b>>=1;
    } return anss;
}

------埃式篩法

------質因數分解

------因數分解

------Dijkstra

------SPFA

------Prim

------trie樹

------KMP匹配

------

【一. 搜尋】

------dfs

(1)dfs常見思路

1.確定dfs的邊界(或剪枝) 2.記憶化搜尋(或剪枝)

3.列舉方向(判斷超界) 4.回溯(所有狀態完全回溯)

vis[xx][yy]=true; dfs(xx,yy,...); vis[xx][yy]=false;

(2)樹上dfs

                          ------可用於lca的pre_dfs

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,遞迴
    }
}

 ------bfs

(1)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】島遊記

(2)

------二分

(1)二分常見思路

1.用於最小化最大值/最大化最小值。 2.設定l、r、mid,進行二分。

3.設定checks函式,判斷是否可行。 4.更新ans,縮小區間l、r。

(2)整數二分、實數二分

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; }

(3)二分圖染色(判定)

------------------------------詳細的分析看 這裡

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;
}

(4)二分圖匹配

<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); //↓↓即最小鏈覆蓋的方案
} //相當於將一開始分開的兩個點合併起來,按照匹配路徑,尋找每條鏈的鏈長

------貪心

(1)平均數:均分紙牌問題...

(2)中位數:貨倉選址問題...

  • 變式:二維轉化為兩個一維考慮,分別取中位數求值。
  • 動態中位數:對頂堆。判斷中位數區間:+1/-1維護字首和。

(3)排序:最小轉化次數...

  • 方法:逆序對 or 每次把最大的放在最後面 or 倒序找逆序個數。

(4)拆分法:把一種物品拆成多個單個物品

  • 例題:【p3049】園林綠化。轉化為01揹包問題。

(5)區間問題:區間覆蓋,區間選點...

  • 方法:維護左右端點,進行排序等操作。
  • 區間型別不同時(如有電壓、燈管...),常把n+m個區間一起排序。

------歸併排序

(1)歸併排序-逆序對模板

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陣列中
}

(2)逆序對個數為k的全排列數量

DP轉移:f[i][j]為前i個數字(即1~i)構成逆序對數為j的方案總數。

全排列逆序對結論:在第k個位置放第i個數,單步得到的逆序對數為 max(0,i-k)

判斷i的插入位置,得到轉移方程:f[i][j]=∑(f[i-1][j-i+1...j-1])。

f[i-1][]的求和可以用字首和陣列維護,同時第一維可以省略(且不需要倒序)。

(3)歸併排序-平面最近點對

  • 思路:先按x座標排序,再用分治法處理y。
double merge(int l,int r){
    double min_dist=INF;
    if(l==r) return min_dist;
    if(l+1==r) return dist(l,r);
    int mid=(l+r)>>1; //分治
    double d1=merge(l,mid),d2=merge(mid+1,r);
    min_dist=min(d1,d2); int i,j,k=0;
    for(i=l;i<=r;i++)
        if(fabs(S[mid].x-S[i].x)<=min_dist)
            ranks[k++]=i;
    sort(ranks,ranks+k,cmps);
    for(i=0;i<k;i++) //注意這裡使用的是0G
        for(j=i+1;j<k&&S[ranks[j]].y-S[ranks[i]].y<min_dist;j++)
            min_dist=min(min_dist,dist(ranks[i],ranks[j]));
    return min_dist; //平面最近點對
}

------離散化

(1)使用 lower_bound排序+去重

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 node{ int x,id; }a[N]; int n,rank[N];

bool cmp(node aa,node bb){ return aa.x<bb.x; }

int main(){ cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i].v,a[i].id=i;
    sort(a+1,a+n+1,cmp); //↓↓得到原順序下每個數的排名
    for(int i=1;i<=n;i++) rank[a[i].id]=i; 
}

【二. 字串】

------字串雜湊

  • H(C)=(c1*b^(m-1)+c2*b^(m-2)+....+cm*b^0) mod h。

b為基數,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值所在的位置(即對應的餘數)。

------KMP模式匹配

題目:給你兩個字串,尋找其中一個字串是否包含另一個字串。

<1>原短字串a的【自我匹配】

  • nextt[i]:原字串的 最長字首 和 (以i結尾的)最長字尾 相同的長度。
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中找到完全匹配)
}

------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樹)

------AC自動機

思想是kmp+trie樹,具體的我還不會...

------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); //最長迴文子串的長度
    }
}

【三. 圖論】

------最小生成樹

<1> Prim演算法

int a[5019][5019],dist[5019],n,w,ans=0;
//↑↑↑把二維的距離陣列a、在每次迴圈中、判斷轉為一維的距離陣列dist
bool vis[5019]; //vis陣列標記點是否訪問過

void prim(){
    memset(dist,0x3f,sizeof(dist)); //0x3f=1061109567
    memset(vis,0,sizeof(vis)); dist[1]=0; //注意設定起點
    for(int i=1;i<n;i++){ //注意:樹只有n-1條邊
        int x=0; for(int j=1;j<=n;j++)
            if(!vis[j]&&(x==0||dist[j]<dist[x])) x=j;
        vis[x]=1; for(int y=1;y<=n;y++)
            if(!vis[y]) dist[y]=min(dist[y],a[x][y]);
    } //每次尋找當前狀態下、到達任意未訪問點需要的最短邊,並更新
}

<2> Kruskal演算法

for(int i=1;i<=m;i++) //存邊
    reads(e[i].x),reads(e[i].y),reads(e[i].w);
for(int i=1;i<=n;i++) fa[i]=i; //初始化
sort(e+1,e+m+1,cmp); //邊權從小到大排序
for(int i=1;i<=m;i++){
    int fx=find_fa(e[i].x),fy=find_fa(e[i].y);
    if(fx!=fy) fa[fx]=fy,ans+=e[i].w; //ans=min/max(ans,e[i].w);
} //也可以統計已加入的邊數,如果達到n-1條邊就退出
  • 難題:【CF76A】國王的禮物(二維+思維+增量最小生成樹)

------最短路

<1> Floyd

  • 用途:推導關係、傳遞閉包。
for(int k=1;k<=n;k++) //過渡層一定要放在最外面
  for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
      d[i][j]=min(d[i][j],d[i][k]+d[k][j]);

    //d[i][j]|=d[i][k]&d[k][j];(傳遞閉包)

<2> Dijkstra

  • 用途:最短路的快速演算法(優先佇列優化)。
priority_queue < pair<int,int> > q;

void dijkstra(int s){
    for(int i=1;i<=n;i++) dist[i]=(int)1e9;
    dist[s]=0,q.push(make_pair(0,s)); //dist的相反數和出發點的編號
    while(q.size()!=0){ //while(!q.empty())
        int x=q.top().second; q.pop();
        if(vis[x]!=0) continue; vis[x]=1;
        for(int i=head[x];i;i=e[i].nextt){
            if(dist[e[i].ver]>dist[x]+e[i].w){
                dist[e[i].ver]=dist[x]+e[i].w;
                q.push(make_pair(-dist[e[i].ver],e[i].ver));
            }
        }
    }
}

<3> SPFA

  • 用途:判負環,差分約束(佇列)。
void spfa(int s){ 
    queue<int>q; //普通佇列(也可以寫成迴圈佇列)
    for(int i=1;i<=n;i++) dist[i]=1e9;
    q.push(s); vis[s]=true; dist[s]=0;
    while(!q.empty()){
      int u=q.front(); q.pop(); vis[u]=false;
      for(int i=head[u];i;i=e[i].nextt)
        if(dist[u]+e[i].w<dist[e[i].ver]){
          dist[e[i].ver]=dist[u]+e[i].w;
          if(!vis[e[i].ver]) //SPFA和dij的區別
            vis[e[i].ver]=true,q.push(e[i].ver);
        }
    }
}

<4> 統計路徑條數

  • 如:【p2047】社交網路/【p1144】最短路計數
  • 相同大小時,累加(floyd累乘);更優時,重新計算。

<5> 最短路徑問題拓展

1.【p2832】行路難,統計權值時要考慮點權。

2. 許多題目需要建立反圖,巧妙處理起點終點的關係。

3.【p1027】Car的旅行路線,多個起點的最短路演算法。

4.【p2939】改造路,分層圖最短路。

------差分約束

對於式子x-y<=b在x,y之間建立長度為b的邊,轉換成最短路問題

建邊:1.a-b>=c,w(b,a)=-c; 2.a-b<=c,w(a,b)=c;

3.a=b,w(a,b)=w(b,a)=0; 4.a-b>c,w(b,a)=-c-1; 5.a-b<c,w(a,b)=c-1;

<1> 給出一些形如x-y<=b不等式的約束,問你滿足條件是否有解

處理:SPFA判負環cnt[v]=cnt[u]+1; if(cnt[v]>n) ...

<2> 給出一些形如x-y<=b不等式的約束,問你滿足條件的最大值

處理:直接求最短路即可。最短路--最大值;最長路--最小值。

------強連通分量

  • tarjan演算法中的常用陣列和變數:
int dfn[N],low[N],stack[N],vis[N];

int dfn_=0,top_=0,sum=0,col[N];

//dfn序,棧中位置top,強連通個數sum,每點所屬連通塊編號col[i]
  • main函式中的迴圈:
for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
  • tarjan主程式:
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]]++; //記錄強連通分量大小
}
  • 無向圖縮點:
for(int i=head[u];i;i=e[i].nextt){
    if(e[i].ver==u_fa) continue; //無向圖縮點與有向圖的區別
    if(!dfn[e[i].ver]) tarjan(e[i].ver,u),low[u]=min(low[u],low[e[i].ver]);
    else if(!col[e[i].ver]) low[u]=min(low[u],dfn[e[i].ver]); //這裡直接用col陣列
}

------拓撲排序

① 從圖中選擇一個入度為0的點加入拓撲序列。 ② 從圖中刪除該結點以及它的所有出邊(即與之相鄰點入度減1)。 ③ 反覆執行這兩個步驟,直到所有結點都已經進入拓撲序列。

  • 拓撲排序判環(入隊的點<n,則出現了環):
//給出n個順序關係,問是否合法。

queue<int>q;

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;
}
  • 將原序列進行拓撲排序,得到新順序:
//給出n個名次關係,求出符合條件的排名順序,輸出字典序最小的答案。

priority_queue< int,vector<int>,greater<int> >q; //小頂堆

int tp_sort(){ //拓撲排序
    for(int i=1;i<=n;i++)
        if(rd[i]==0) q.push(i);
    while(!q.empty()){
        x=q.top(),q.pop(),cnt++,ans[cnt]=x;
        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);
        }
    }
}

------LCA

  • pre_dfs函式確定fa(即f[u][0]):
void pre_dfs(int u,int fa_){

    for(int i=0;i<=19;i++) f[u][i+1]=f[f[u][i]][i],
        w[u][i+1]=min(w[u][i],w[f[u][i]][i]);
    //↑↑維護lca路徑上的最小值,注意w陣列不需要初始化
    
    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; //距離
        f[v][0]=u,w[v][0]=e[i].w,pre_dfs(v,u);
    }
}
  • 找LCA的主程式(確定lca):
int lca(int x,int y){ //找lca的主程式
    
    int anss=(int)1e9; //找到lca路徑上的最短邊

    if(dep[x]<dep[y]) swap(x,y); //保證dep[x]>dep[y]
    
    for(int i=20;i>=0;i--){ //注意:這裡的20和上面的19都是log2n的近似取值
        if(dep[f[x][i]]>=dep[y]) anss=min(anss,w[x][i]),x=f[x][i];
        //↑↑↑i的2^k輩祖先的結點仍比y深,令x=f[x,i],繼續向上跳
        if(x==y) return anss; //若x=y,則已經找到了lca
    }

    for(int i=20;i>=0;i--) //↓↓↓未找到lca時的倍增跳法
        if(f[x][i]!=f[y][i]) //更新次路徑上的最短邊,並繼續向上跳
            anss=min(anss,min(w[x][i],w[y][i])),x=f[x][i],y=f[y][i];

    lca=f[x][0]; //如果只需要找lca,直接返回f[x][0]即可
    
    return anss=min(anss,min(w[x][0],w[y][0])); //路徑上的最小邊
}

【四. 資料結構】

------樹狀陣列

<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; //返回二維字首和
}
  • ans=query(xx,yy)-query(xx,y-1)-query(x-1,yy)+query(x-1,y-1);

<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) 即可。

<6> 二維 —— 區間修改,區間查詢  過/於/復/雜/暫/不/簡/述...

------線段樹

<1> 線段樹維護區間最值/區間和

線段樹結構體(陣列要開4倍):

struct SegmentTree{ int l,r,sum; }tree[4*N];

build-建樹函式:

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); //將修改值向上傳遞
}

PushUp-上移函式:

void PushUp(int rt){ tree[rt].sum=tree[rt<<1].sum+tree[rt<<1|1].sum; }

add-單點修改函式:

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;//所有包含結點rt的結點狀態更新
}

query-單點查詢函式:

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);
}

sum-區間查詢函式:

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);
}

<2> 線段樹維護最大的可行區間(【p2894】酒店)

需要維護三個最大值:

1.這個區間內最多的連續空格的個數.ans。
2.這個區間從左端點開始向右的空格的個數.l。
3.這個區間從右端點開始向左的空格的個數.r。

結構體改成: struct node{ int l,r,tag,ans; }tree[N];

void PushUp(int l,int r,int rt){
    int mid=(l+r)>>1,ls=(rt<<1),rs=(rt<<1|1);
    tree[rt].l=(tree[ls].ans==mid-l+1)?(tree[ls].ans+tree[rs].l):tree[ls].l;
    tree[rt].r=(tree[rs].ans==r-mid)?(tree[rs].ans+tree[ls].r):tree[rs].r;
    tree[rt].ans=max(tree[ls].ans,tree[rs].ans);
    tree[rt].ans=max(tree[rt].ans,tree[ls].r+tree[rs].l);
}

線段樹中最重要的就是PushDown函式:

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; //標記每次下移一位,並清空上一位置的標記
}

注意,修改函式和詢問函式中,如果沒有到達終點,就要不斷PushDown

------分塊

主要思想每整塊標記tag,剩下的l、r兩個邊界塊直接修改

分塊大小:m=sqrt(n); 方式:for(i=1~n) pos[i]=(i-1)/m+1;

難題:【區間開方取整+區間求和】okk陣列記錄每個整塊中的元素是否全部<=1。

難題:【單點插入+單點詢問】暴力插入,如果某塊太大,需要重新分塊

難題:【查詢區間最小眾數】f[i][j]記錄第i到j塊的眾數,vector存每種數出現的所有位置

【五. 動態規劃】

------線性DP

<1> LIS:最長上升子序列

for(int i=1;i<=n;i++) f[i]=1;
for(int i=2;i<=n;i++)
    for(int j=i-1;j>=f[i];j--)
    //↑↑↑【剪枝】j>=f[i]:如果j小於目前長度,不可能使答案更新
        if(a[j]<a[i]) f[i]=max(f[i],f[j]+1);
for(int i=1;i<=n;i++) ans=max(ans,f[i]);
O(n*logn)

for(int i=1;i<=n;i++) reads(a[i]);
for(int i=1;i<=n;i++){
    if(a[i]>list[len])
     { list[++len]=a[i]; continue; } 
    int pos=lower_bound(list+1,list+len+1,a[i])-list;
    list[pos]=a[i];
} printf("%d\n",len);

 <2> LCS:最長公共子序列

for(int i=1;i<=lens;i++)
    for(int j=1;j<=lent;j++){
        f[i][j]=max(f[i-1][j],f[i][j-1]);
        if(s[i]==t[j]) //如果元素相同
            f[i][j]=max(f[i][j],f[i-1][j-1]+1);
    } //注意:為了防止陣列下標出現-1,輸入時要用ss+1
printf("%d\n",f[lens][lent]);
O(n*logn)

for(int i=1;i<=n;i++)
    reads(a1[i]),id[a1[i]]=i;
for(int i=1;i<=n;i++) reads(a2[i]);
for(int i=1;i<=n;i++){
    if(id[a2[i]]>list[len]){
        list[++len]=id[a2[i]]; continue;
    } int k=lower_bound(list+1,list+len+1,id[a2[i]])-list;
    list[k]=id[a2[i]];
} printf("%d\n",len);

 ------揹包DP

<1> 0/1揹包

注意:dp陣列初始化為maxx,設定起點dp[0]=0;

for(int i=1;i<=n;i++)
    for(int j=m;j>=v[i];j--) //注意:一定要倒序迴圈
        dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
for(int j=0;j<=m;j++) ans=max(ans,dp[j]);

<2> 完全揹包

for(int i=1;i<=n;i++)
    for(int j=v[i];j<=m;j++) //注意:一定要正序迴圈
        dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
for(int j=0;j<=m;j++) ans=max(ans,dp[j]);

<3> 分組揹包

for(int i=1;i<=n;i++) //每組只能選一個
  for(int j=m;j>=0;j--) //倒序
    for(int k=1;k<=c[i];k++) //注意:迴圈順序與多重揹包相反
    //↑↑↑這裡的個數迴圈要放在容量迴圈的後面,才能保證唯一性
      if(j>=v[i][k]) f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);

<4> 多重揹包

for(int i=1;i<=n;i++)
  for(int j=1;j<=s[i];j++) //先列舉個數
    for(int k=m;k>=v[i];k--) //列舉總體積、倒序迴圈
      dp[k]=max(dp[k],dp[k-v[i]]+w[i]);
for(int j=0;j<=m;j++) ans=max(ans,dp[j]);

【拓展】多重揹包的二進位制拆分

  • 把多重揹包的第i種物品看成獨立的 k(log(s[i]))+2 個物品,轉化為0/1揹包。
  • p[i]=2^0+2^1+...+2^k+r[i](多餘部分),用這些物品可以表示出這種物品所有<=p[i]的數量。

二進位制拆分:

void broke(){ //【多重揹包的二進位制拆分】
    for(int i=1;i<=n;i++){
        int cnt=1; //cnt=2^k
        while(s[i]!=0){ //沒拆分完
            ww[++num]=w[i]*cnt;
            vv[num]=v[i]*cnt; //價值和代價都要*cnt
            s[i]-=cnt; cnt=cnt<<1; //cnt*2
            if(s[i]<cnt){ //多出來的部分(即r[i])
                ww[++num]=w[i]*s[i];
                vv[num]=v[i]*s[i]; break;
            }
        }
    }
}

轉化為01揹包:

for(int i=1;i<=num;i++) //拆分後的物品個數
    for(int j=m;j>=vv[i];j--) f[j]=max(f[j],f[j-vv[i]]+ww[i]);

------區間DP

一個狀態、由若干個比它更小、且包含於它的區間、所代表的狀態轉移而來。

【區間DP的狀態轉移方法】

  • 從小到大列舉區間長度,列舉對應長度的區間。
for(int len=1;len<=N;++len) //區間長度
    for(int l=1,r=len;r<=N;++l,++r)
        { 考慮F[l][r]的轉移方式 }

基本決策(列舉斷點):dp[i][j]=min{ dp[i][k]+dp[k+1][j] | i<=k<j };

【求dp[i][j]具體步驟】(p4170-塗色)

  • 當i==j時,子串明顯只需要塗色一次,於是dp[i][i]=1
  • 當i!=j且s[i]==s[j]時,可以直接繼承之前的狀態,於是dp[i][j]=min(dp[i][j-1],dp[i+1][j])
  • 當i!=j且s[i]!=s[j]時,列舉子串的斷點k,於是dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j])

難題:括號配對 / p1005-矩陣取數...

------環形DP

【破環為鏈】for(int i=1;i<=N;i++) v[N+i]=v[i]; //注意:陣列要開2*N大小。

------樹形DP

<1> 0/1型樹形DP

又稱樹的最大獨立集問題。比如【p2016-戰略遊戲】

當前節點選或不選,父親節點選或不選,兒子節點選或不選。

規劃所有狀態,遞迴子樹,判斷轉移,得出最終的答案。

<2> 揹包類樹形DP

又稱有樹形依賴的揹包問題。比如【p2014-選課】。

除了以 “節點編號” 作為樹形DP的階段(第一維度),

還要把當前揹包的 “體積” 作為第二維狀態。

------數位DP

區間可減性:ans(l,r)=sum(r)-sum(l-1);

int counts(int x){ //儲存上界x的每一位
    int len=0;
    while(x) bit[len++]=x%10,x/=10;
    return dfs(len-1,0,true);
} //注意是len-1,在dp函式中邊界為:pos=-1

數位DP的主函式:

int dfs(int pos,int sum,bool limit){
    if(pos==-1) return (sum==0); //是否是mod的倍數
    if(!limit&&f[pos][sum]) return f[pos][sum]; //記憶化
    int end=(limit==1)?bit[pos]:9,ans=0;
    for(int i=0;i<=end;i++){ //end是當前上界
        int newsum=(sum+i)%mod; //數字和
        ans+=dfs(pos-1,newsum,limit&&(i==end));
    } if(!limit) f[pos][sum]=ans; return ans;
} //當前情況下不用判斷前導0,但有些時候需要判斷

------狀壓DP

一個集合內的元素資訊作為狀態、且狀態總數為指數級別的DP。

(1)具體解題模式:

  • 【找狀態】確定每行的M位二進位制數中0、1的表示。
  • 【存已知】存入時把初始[每行]的二進位制狀態變為一個十進位制的數,便於數位操作。
  • 【預處理】結合輸入求出[每行]的[所有滿足可行性]的M位二進位制數。
  • 【判邊界】一般行列間的關係在[起始行]並不適用,要[特殊處理]第一行的狀態。
  • 【列方程】逐層列舉每行和上一行的狀態,[判斷行列關係],列狀態轉移方程。

(2)常用轉移方程:

if((s&(1<<(j-1)))&&(s&(1<<(i-1)))) //j是當前的結尾節點
    f[s][j]=min(f[s][j],f[s^(1<<(j-1))][i]+cost[i][j]);

列舉最後作為結尾的結點:ans=min(f[(1<<n)-1][i]);

(3)常用的二進位制操作:

1.每行選幾個:取二進位制狀態下1的個數,用counts函式。

int counts(int x){ int cnt=0; while(x) x&=(x-1),cnt++; return cnt; }

2.每行每列沒有相鄰的:列舉所有狀態,!(i&(i<<1)) 時才是每行的可行狀態,存入state[ ]中。

3.初始化第一行的情況,轉移時,列舉此行的狀態和上一行的狀態,行間需要滿足:

if(!(state[now]&state[last])&&j>=sum[now]) //j:前i行選的個數

------單調佇列優化DP

1、維護隊首可行性,head++; 2、維護隊尾單調性,並插入當前元素; 3、取出隊頭的最優解,進行DP轉移。

int head=1,tail=1; //手寫優先佇列

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]加入隊尾          //單調遞減佇列:如果後方有數更大,前面就刪除; }

------斜率優化DP

對於每個斜率方程(Y(j2)-Y(j1))/(X(j2)-X(j1)):

1.將資料進行預處理(求sum等操作),優化序列。 2.寫狀態轉移方程,如果是二維,要使用二維單調佇列。 3.推導不等式,化成斜率的一般式,一般使用化除為乘。 4.從而得到X,Y的定義式,用double型別表示出來。 5.建立一個類似優先佇列的斜率單調佇列。 6.維護頭尾可行性以及斜率單調性,隊頭為最優答案。

【六. 數論】

<1> 取整除法求和

ll ans(ll n){ //O(√n)     ll anss=0,nn=sqrt(n) ;     for(ll i=1;i<=nn;i++) anss+=n/i;     return (anss<<1)-nn*nn; } //先處理<=√n的,剩下的用公式推出

<2> N的正約數集合

int factor[2519],cnt=0;

for(int i=1;i*i<=n;i++){     if(n%i==0){         factor[++cnt]=i;         if(i!=n/i) factor[++cnt]=n/i;     } }

<3> 最大公約數GCD

int gcd(int a,int b){ //歐幾里得演算法,O(log(a+b)).     //if(a<b) swap(a,b); //保證a>=b     return (b==0)?a:gcd(b,a%b); }

難題:高精度版最大公約數(用二進位制演算法)

<4> 求解不定方程---EXGCD

int exgcd(int a,int b,int &x,int &y){     if(b==0){ x=0; y=1; return a; } //b<=a     int gcd_=exgcd(a%b,b,y,x); //gcd_是a、b的最大公約數     x-=(a/b)*y; return gcd_; //x=x0-[a/b(下取整)]*y0; y=y0; }

int cal(){ //a*x+b*y=c     int gcd_=exgcd(a,b,x,y);     if(c%gcd_!=0) return -1; //不可能有解     x*=c/gcd_,b/=gcd_;     if(b<0) b=-b; int ans=x%b;     if(ans<=0) ans+=b; return ans; } //注意ans=0的情況↑↑

<5> 埃式篩質數

int vis[N],primes[N],cnt=0;

void init(int x){   for(int i=2;i<=x;i++)     if(!vis[i]){       primes[cnt++]=i;       for(int j=i+i;j<=x;j+=i)         vis[j]=1;     } }

<6> 分解質因數

void init(int x){ int cnt=0;     for(int i=2;i*i<=x;i++)       while(x%i==0) primes[++cnt]=i,x/=i;     if(x>1) primes[++cnt]=x; }

<7> 快速冪

ll ksm(ll a,ll b,ll mod){     ll anss=1; //注意初始化為1     while(b>0){ //求a的b次方%mod         if(b&1) anss=anss*a%mod;         a=a*a%mod; b>>=1;     } return anss; }

<8> 乘法逆元

ll inv1(ll a,ll mod){ //擴充套件歐幾里得求逆元       ll x,y; ll d=exgcd(a,mod,x,y);     if(d==1) return (x%mod+mod)%mod; return -1; }

ll inv2(ll a,ll mod){ return ksm(a,mod-2,mod); } //費馬小定理

void inv3(ll mod){ inv[1]=1; //線性遞推求逆元       for(int i=2;i<=mod-1;i++) //求1~n的逆元       inv[i]=(mod-mod/i)*inv[mod%i]%mod,cout<<inv[i]<<" "; }

<9> 組合數

<10> Lucas定理

<11> Nim遊戲

如果每一堆石子的個數異或起來的值不為0,那麼先手必勝,

如果為0,那麼先手必敗。

for(int i=1,x;i<=n;reads(x),ans^=x,i++); if(ans) puts("Yes"); else puts("No");