模板整理——by hkhh
非圖論:
一、二分有關
1):二分查找
手敲模板:
int find(int x){//假設從小到大排序,找第一個大於等於x的數
int l = 1 , r = n;
while (l+1<r){
int mid = l+r >> 1;
if (a[mid] == x) return mid;//如果找到,返回位置
if (a[mid] > x) r = mid;
else l = mid;
}
if (a[l]>=x) return l;
else if (a[r]>=x) return r;
else return -1;//找不到
}
stl自定義:
1.lower_bound(a+1,a+n+1,num)-a //尋找a數組裏面第一個>=x的數的位置
2.upper_bound(a+1,a+n+1,num)-a //尋找a數組第一個>x的數的位置
vector 中: lower_bound(a.begin(),a.end(),num)-a.begin() // 在vector< int > a中尋找第一個>=num的數的位置
2):二分答案:
int l = 1 , r = n;
while (l+1 < r){
int mid = l+r>>1;
if (check(mid)) l = mid;
else r = mid;//這裏的操作根據不同題意而定
}
if (check(l)) return l;else return r;
二、樹狀數組
1)定義(好像也沒啥定義可以說):
樹狀數組主要是基於二進制思想,通過lowbit(刪數二進制位的最後一位0)來實現前綴和的累加操作,其實是將一段連續的區間分成幾段由2的整次冪所組成的區間的和,這樣便使線性複雜度變爲log複雜度
2)主要的幾個函數
lobiwt函數(核心)
#define lowbit(x) x&-x
求和函數
int Ask(int x){
int ans = 0;
for (int i = x;i;i-=lowbit(i))
ans+=t[i];
return ans;
}
修改函數
void Change(int x,int v){
for (int i = x;i <= maxx;i+=lowbit(i)) t[i]+=v;
}
代碼實現:
#define lowbit(x) x&-xint Ask(int x){
int ans = 0;
for (int i = x;i;i-=lowbit(i))
ans+=t[i];
return ans;
}
void Change(int x,int v){
for (int i = x;i <= maxx;i+=lowbit(i)) t[i]+=v;
}
3)稍加改變
區間修改可以用查分實現
Change(i,v);
Change(j+1,-v);
然後這個差分數組也可以用樹狀數組維護
二維樹狀數組
搞一搞就好
三、並查集
1)用途:主要爲了維護圖的連通性以及集合之間的關係
2)優化:路徑壓縮可以極大的優化時間
3)幾個函數:
查找祖先
int getfa(int k){
return fa[k] == k?k:fa[k] = getfa(fa[k]);
}
合併以及判斷連通性
int x = getfa(x) , y = getfa(y);
if (x != y) fa[x]=y;//如果不連通,就並在一起
四、數學有關
1)六個函數:
int add(int a,int b){return a+b>=P?a+b-P:a+b;}
void Add(int &a,int b){a = add(a,b);}
//加
int sub(int a,int b){return a-b<0?a-b+P:a-b;}
void Sub(int &a,int b){a = sub(a,b);}
//減
int mul(int a,int b){return 1ll*a*b%P;}
void Mul(int &a,int b){a = mul(a,b);}
//乘
這樣似乎就可以防止因爲一些精度、模數問題而爆炸了
2)快速冪
int quickm(int x,int y){//x^y
int ans = 1;
for ( ; y ;y>>=1 , Mul(x,x))
if (y&1) Mul(ans,x);
return ans;
}
3)快速乘(其實是龜速乘(複雜度爲log),主要是防止爆long long)
int quickc(int x,int y){
int ans = 0;
for ( ; y ;y>>=1 , Add(x,x))
if (y&1) Add(ans,x);
return ans;
}
4) 最大公因數
int gcd(int x,int y){
return x%y == 0?y:gcd(y,x%y);
}
五、分塊相關
這裏給出基本的區間修改,區間查詢的板子
0)變量解釋:
1)基本分塊:
分塊(預處理)
int t = sqrt(n);
for (int i=1;i<=t;i++){
L[i] = (i-1)*t+1;
R[i] = i*t;
}//將區間分成sqrt(n)段
if (R[t] < n) L[++t] = R[t-1]+1,R[t] = n;//可能會有剩下的
for (int i=1;i<=t;i++)
for (int j=L[i];j<=R[i];j++)
pos[j] = i , sum[i]+=a[j];//記錄每一個點屬於的塊的編號,求出初始的時候每一個塊的和
修改操作
void Change(int l,int r,int v){
int p = pos[l] , q = pos[r];
if (p == q){
for (int i=l;i<=r;i++) a[i]+=v;
sum[p]+=(r-l+1)*v;
}//如果不在一個塊內,直接暴力求
else {
for (int i=p+1;i<=q-1;i++) add[i]+=v;//中間包含的塊可以直接求
for (int i=l;i<=R[p];i++) a[i]+=v;
sum[p]+=(R[p]-l+1)*v;
for (int i=L[q];i<=r;i++) a[i]+=v;
sum[q]+=(r-L[q]+1)*v;//兩邊暴力求
}
}
求和操作
int Ask(int l,int r){
int ans = 0;
int p = pos[l] , q = pos[r];
if (p == q){
for (int i=l;i<=r;i++) ans+=a[i];
ans+=add[p]*(r-l+1);
}
else {
for (int i=p+1;i<=q-1;i++) ans+=sum[i],ans+=add[i]*(R[i]-L[i]+1);
for (int i=l;i<=R[p];i++) ans+=a[i];ans+=add[p]*(R[p]-l+1);
for (int i=L[q];i<=r;i++) ans+=a[i];ans+=add[q]*(r-L[q]+1);
}
return ans;
}//同樣操作
2)分塊改進:莫隊
原因:對於有限、簡單的區間修改、區間查詢問題,分塊是很優秀的。但是當區間查詢的量到達一定程度、需要維護的量變得不好處理的時候,分塊就顯得乏力了。在這種情況下,我們引入莫隊
基本思想:
- 離線處理,將詢問進行分塊,對分塊後的詢問進行操作,通過刪除、增加區間來得到答案。由於我們已經將詢問排序,所以複雜度基本是線性的。
以 HH的項鍊爲例
變量名解釋:
//1.預處理,將詢問分塊
for (int i=1;i<=m;i++){
scanf("%d %d",&q[i].l,&q[i].r);
q[i].nm = (q[i].l+len-1)/len;
q[i].id = i;
}
//排序
bool mycmp(node x,node y){if (x.nm == y.nm) return x.r<y.r;else return x.nm<y.nm;}
sort(q+1,q+m+1,mycmp);
//對區間進行修改
int nowl = 1 , nowr = 0 , ans = 0;
for (int i=1;i<=m;i++){
while (l<q[i].l) Del(l++);
while (l>q[i].l) Add(--l);
while (r<q[i].r) Add(++r);
while (r>q[i].r) Del(r--);
}//這裏的Del , Add 都是具體的操作,因題而異
六、線段樹:
1)定義及性質:
- 線段樹也是一種基於二進制而來的一種數據結構,主要用來維護區間上的一些操作。
- 線段樹的每一個節點都代表着一個區間;線段樹的葉子結點都代表着一個長度爲1的區間。
- 對於一個節點,它的左兒子的編號是,右兒子是.
- 對於一個節點x包含區間,它的左兒子包含區間[l,mid],右兒子是. 其中
2)基本函數
變量解釋:
struct Node{int l,r,sum,add;}
struct Tr{
Node t[4*N];
#define sum(x) t[p].sum
#define l(x) t[p].l
#define r(x) t[p].r
#define ls p<<1
#define rs p<<1|1
void build(int p,int l,int r){
l(p) = l , r(p) = r , sum(p) = 0;
if (l == r) {sum(p) = a[l];return;}//遍歷到葉子結點
int mid = l+r>>1;
build(ls,l,mid),build(rs,mid+1,r);
sum(p) = sum(ls) + sum(rs);
}
void spread(int p){
if (!add(p)) return;
sum(ls)+=add(p)*(r(ls)-l(ls)+1);
add(ls)+=add(p);
sum(rs)+=add(p)*(r(rs)-l(rs)+1);
add(rs)+=add(p);
add(p) = 0;//下傳完延遲標記之後清空
}
void Change(int p,int l,int r,int v){//給[l,r]都加上v
if (l<=l(p) && r(p)<=r) {
sum(p)+=(r(p)-l(p)+1)*v;
add(p)+=v;
return;
}
spread(p);
int mid = l(p) + r(p) >> 1;
if (l<=mid) Change(ls,l,r,v);
if (r>mid) Change(rs,l,r,v);
sum(p) = sum(ls)+sum(rs);
}
int Ask(int p,int l,int r){//詢問[l,r]的區間和
if (l<=l(p) && r(p)<=r) return sum(p);
spread(p);
int mid = l(p) + r(p) >> 1;
int ans = 0;
if (l<=mid) ans+=Ask(ls,l,r);
if (r>mid) ans+=Ask(rs,l,r);
return ans;
}
};//定義到結構體裏面調用更加方便
對於其他的線段樹的變形都是基於這個模板的
七、高精度
1)高精加
int a[Maxx] , b[Maxx];
string s1,s2;
cin>>s1>>s2;
int len1 = s1.size() , len2 = s2.size();
for (int i = 1; i <= s1.size(); i++) a[i] = s1[len1-i]-48;
for (int i = 1; i <= s2.size(); i++) b[i] = s2[len2-i]-48;
int len = max(len1 , len2);
for (int i = 1; i <= len; i++){
c[i] = a[i]+b[i];
c[i+1] += c[i]/10;
c[i]%=10;
}
int k = Maxx-1;
while (!c[k] && k>1) k--;
for (int i = k; i >= 1; i--) cout<<c[i];
2)高精減
int a[Maxx] , b[Maxx];
string s1,s2;
cin>>s1>>s2;
if (s1比s2小) cout<<'-',swap(s1,s2);
for (int i = 1; i <= s1.size(); i++) a[i] = s1[len1-i]-48;
for (int i = 1; i <= s2.size(); i++) b[i] = s2[len2-i]-48;
int len = max(len1 , len2);
for (int i = 1; i <= len; i++){
c[i]+=a[i]-b[i];
if (c[i] < 0) c[i]+=10 , c[i+1]-=1;
}
3)高精度乘法
for (int i = 1; i <= len1; i++)
for (int j = 1; j<= len2; j++){
c[i+j-1]+=a[i]*b[j];
c[i+j]+=c[i+j-1]/10;
c[i+j-1]%=10;
}
八、線性篩
f[1] = 1;
for (int i = 2; i <= Maxx; i++)
if (!f[i])
for (int j = i+i; j <= Maxx; j+=i) f[j] = 1;
九、ST表
,這裏就設爲最大
1) ST表預處理
for (int i = 1; i<=n; i++) f[i][0] = a[i];
int len = log2(n)+1;
for (int j = 1; j<len; j++)
for (int i = 1; i<=n-(1<<j)+1; i++)
f[i][j] = max(f[i][j-1] , f[i+(1<<j-1)][j-1]);
2) ST表詢問
int Ask(int l,int r){//詢問[l,r]的最大值
int k = log2(r-l+1);
return max(f[i][k] , f[r-(1<<k)+1][k]);
}
十、快讀
#define gc getchar()
int read(){
int s = 0 , f = 0;char ch = gc;
while (ch<'0' || ch>'9') f|=ch == '-' , ch = gc;
while (ch>='0' && ch<='9') s = s*10+ch-48 , ch = gc;
return f?-s:s;
}
圖論:
一、最短路
1)Dijkstra 堆優化
typedef pair < int , int > pii;
#define mp make_pair
priority_queue < pii , vector < pii > , greater < pii > > q;
memset(dis , 20 , sizeof dis);
dis[st] = 0;
q.push(mp(dis[st],st));
while(!q.empty()){
int v = q.top().first , x = q.top().second;
q.pop();
if (vis[x]) continue;
vis[x] = 1;
for (int i = linkk[x]; i ; i = e[i].Next)
if (v+e[i].v < dis[e[i].y]) dis[e[i].y] = v+e[i].v , q.push(mp(dis[e[i].y],e[i].y));
}
2)SPFA
memset(vis,0,sizeof vis);//標記當前點是否在隊列中
memset(dis,20,sizeof dis);
dis[1] = 0 ,vis[1] = 1;
queue < int > q;
q.push(1);
while (!q.empty()){
int x = q.front();q.pop();
vis[x] = 0;
for (int i = linkk[x]; i; i = e[i].Next){
int y = e[i].y;
if (d[x] + e[i].v < d[y]){
d[y] = d[x] + e[i].y;
if (!vis[y]) vis[y] = 1 , q.push(y);
}
}
}
3) Floyed
for (int k = 1; k<=n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
f[i][j] = min(f[i][k]+f[k][j],f[i][j]);
二、最小生成樹
1)Kruscal
bool mycmp(Node x,Node y){return x.v < y.v;}
int getfa(int k){return k == fa[k]?k:fa[k] = getfa(fa[k]);}
for (int i = 1; i <= n; i++) fa[i] = i;
sort(e+1,e+m+1,mycmp);
int ans = 0 , cnt = 0;
for (int i = 1; i <= m; i++){
int x = getfa(e[i].x) , y = getfa(e[i].y);
if (x == y) continue;
ans+=e[i].v;
fa[x] = y;
cnt++;
if (cnt == n-1) break;
}
//prim 不想寫了,思路跟dij差不多
三、最近公共祖先
1)樹上倍增:
思路:
- 通過dfs記錄每一個點的父親以及深度,從而得到他上面輩的點,從而對於兩個點,我們可以不斷的跳倍增來實現,複雜度,而且好理解
變量解釋:
表示節點i向上輩的父親的編號
表示編號爲i的結點的深度
代碼實現:
void dfs(int x,int faa,int dd){
d[x] = dd;
for (int i = linkk[x]; i; i = e[i].Next){
int y = e[i].y;
if (y == fa) continue;
fa[y][0] = x;
dfs(y,x,dd+1);
}
}//預處理深度以及父親
void find_fa(){
for (int j = 1; j <= 25; j++)
for (int i = 1; i <= n; i++)
fa[i][j] = fa[fa[i][j-1]][j-1];
}//狀態轉移
int lca(int u,int v){
if (d[u] < d[v]) swap(u,v);
for (int dd = d[u]-d[v] , i = 0; dd; dd>>=1 , i++)
if (dd&1) u = fa[u][i];
if (u == v) return u;
for (int i = 25; i>=0; i--)
if (fa[u][i] != fa[v][i]) u = fa[u][i] , v = fa[v][i];
return fa[u][0];
}//樹上倍增求lca
2) tarjan:
- 不會
四、基環樹
1)定義:
基環樹是指點數等於邊數的樹,其實就是環套樹
2)如何求解、找環:
基環樹的核心其實就是找到那個環,只要我們找到環,就可以進行一系列操作。
找環其實很簡單,我們只要遍歷一遍,發現遍歷到遍歷到的點時,就可以通過跳躍父節點把環找出來
3)代碼實現
void find_root(int x,int faa){
if (Flag) return;//找到環就退出
vis[x] = 1;
for (int i = linkk[x]; i; i=e[i].Next){
int y = e[i].y;
if (y == faa) continue;
if (Flag) return;//這裏需要加一步,否則會炸掉
if (vis[y]){
Flag = 1;
rt[++cnt] = y;
for (int j = x; j != y; j = fa[j]) rt[++cnt] = j;
return;
}
else fa[y] = x,find_root(y,x);
}
}
五、有向圖的拓撲序
1)用途:
1.有向圖的拓撲序常常可以用來解決有向圖的一些狀態轉移,所以常用有向圖的拓撲將有向圖轉換成數列來進行dp
3.同時有向圖的拓撲序還可以判環
2)方法:
找到入度爲0的點加入隊列,刪除它相連的所有邊,繼續將入度爲0的點加入隊列,繼續刪……重複這一操作。
如果剩餘還有點存在,那麼他們一定是環
3)代碼實現:
int cnt = 0;
queue < int > q;
for (int i = 1; i <= n; i++)
if (!in[i]) q.push(i);
while (!q.empty()){
cnt++;
int x = q.front();q.pop();
for (int i = linkk[x]; i; i = e[i].Next){
int y = e[i].y;
in[y]--;
if (!in[y]) q.push(y);
}
}
if (cnt == n) 無環
else 有環
六、圖的割點與割邊
割邊:
定義:對於,在途中刪去邊後,若圖分裂成了兩個不相連的子圖,則稱爲的割邊或橋
1)變量解釋:
由此我們可以得到,當時,說明當前點不能通過這條邊走到更早的邊,也就是說這條邊就是割邊
2)代碼實現:
//len 初值爲1
void Tarjan(int x,int laste){
dfn[x] = low[x] = ++cnt;//給結點的遍歷順序編個號(其實就是dfs序)
for (int i = linkk[x]; i; i = e[i].Next){
int y = e[i].y;
if (!dfn[y]){
Tarjan(y,i);
low[x] = min(low[x],low[y]);
if (low[y] > dfn[x]) isb[i] = isb[i^1] = 1;//說明是割邊
}
else low[x] = min(low[x],dfn[y]);
}
}
割點
大體與割邊的思想相同,只不過當時就爲割點,因爲此時當前點是被包含的,刪數仍然不行
代碼實現
void Tarjan(int x){
int Flag = 0;
dfn[x] = low[x] = ++cnt;
for (int i = linkk[x]; i; i = e[i].Next){
int y =e[i].y;
if (!dfn[y]){
Tarjan(y);
low[x] = min(low[x] , low[y]);
if (low[y] >= dfn[x]){
Flag++;
if (x != Root || Flag > 1) isc[x] = 1;//如果根節點的出邊爲1,那麼便不是割點
}
}
else low[x] = min(low[x] , dfn[y]);
}
}
七、二分圖
1)定義:
如果一張無向圖的個節點()可以分成,兩個集合,是在同一個集合內的點沒有邊相連,那麼稱這一張無向圖爲二分圖
一、二分圖的匹配:選取二分圖中的邊集,當且僅當集合中的任意兩條邊都沒有共同公共端點時,S爲二分圖的一個匹配
二、匹配邊/未匹配邊:在集合S中的邊稱爲匹配邊,除此之外的邊稱爲未匹配邊
三、未蓋點(未匹配點)/匹配點:匹配邊相連的端點稱爲匹配點,除此之外的稱爲未蓋點(未匹配點)
四、交錯路(增廣路):任意兩條相鄰的邊一定是一條匹配邊另一條爲匹配邊的路稱爲交錯路
五、可增廣路:端點爲未蓋點的交錯路(增光路)稱爲可增廣路
六、可增廣路的性質:
- 長度爲奇數(兩端都要是非匹配點)
- 第條邊都是非匹配邊,而都是匹配邊,也就是說,我們只要把狀態反一反,就可以使匹配數+1
- 所以我們得到以下結論:若二分圖的匹配S使最大匹配,當且僅當S不是增廣路
二、二分圖的最大匹配
- 二分圖匹配其實就是找增廣路的個數然後將他變成可增廣路使得匹配數+1
這裏需要介紹匈牙利算法(增廣路算法):
1、設集合爲空,即此時一條匹配邊都沒有
2、找出圖中的增廣路,將邊取反,即匹配邊變成非匹配邊,非匹配邊變成匹配邊,此時匹配個數便會在原基礎上+1
那麼這裏的關鍵就是如何求增廣路。
當我們想匹配(x,y)時,滿足以下條件之一的時候,易證找出了一條增廣路:
1、y本身便是非匹配點,此時直接可以連邊組成長度爲1的增廣路
2、y已經連了,但可以匹配到另一個新的點,那麼此時便可以讓去匹配,與便可以匹配
用便可解決。
但匈牙利更是一個基於貪心的算法,它認爲:當一個點已經成爲匹配點時,只會找到增廣路使總的匹配數增多,而不會使當前點成爲非匹配點。
顯然是正確的(其實我也不是到爲什麼是正確的 )
代碼實現:
bool dfs(int x){
for (int i=linkk[x];i;i=e[i].Next){
int y = e[i].y;
if (vis[y]) continue;
vis[y] = 1;
if (!match[y] || dfs(match[y])) {
match[y] = x;match[x] = y;return 1;
}
}
return 0;
for (int i=1;i<=n;i++) memset(vis,0,sizeof vis),ans+=dfs(i);
}