- 語法
- 常規算法
- 計算幾何
- 數據結構
- 圖論
- 字符串
- 雜項
- 數學
語法
C++11
初始代碼
#include <bits/stdc++.h>
using namespace std;
#define repeat(i,a,b) for(int i=(a),_=(b);i<_;i++)
#define repeat_back(i,a,b) for(int i=(b)-1,_=(a);i>=_;i--)
#define mst(a,x) memset(a,x,sizeof(a))
#define fi first
#define se second
#define endl "\n"
mt19937 rnd(chrono::high_resolution_clock::now().time_since_epoch().count());
int cansel_sync=(ios::sync_with_stdio(0),cin.tie(0),0);
const int N=200010; typedef long long ll; const int inf=~0u>>2; const ll INF=~0ull>>2; ll read(){ll x; if(scanf("%lld",&x)==-1)exit(0); return x;} typedef double lf; const lf pi=acos(-1.0); lf readf(){lf x; if(scanf("%lf",&x)==-1)exit(0); return x;} typedef pair<ll,ll> pii; template<typename A,typename B>void operator<<(A &a,B b){a.push_back(b);}
const ll mod=(1?1000000007:998244353); ll mul(ll a,ll b,ll m=mod){return a*b%m;} ll qpow(ll a,ll b,ll m=mod){ll ans=1; for(;b;a=mul(a,a,m),b>>=1)if(b&1)ans=mul(ans,a,m); return ans;}
#define int ll
void Solve(){
}
signed main(){
//freopen("data.txt","r",stdin);
int T=1; T=read();
while(T--)Solve();
return 0;
}
放在本地的內容(比如可以放進 bits/stdc++.h
)(當然修改了頭文件還要編譯一下)
template<typename A,typename B>
std::ostream &operator<<(std::ostream &o,const std::pair<A,B> &x){
return o<<'('<<x.first<<','<<x.second<<')';
}
#define qwq [&]{cerr<<"qwq"<<endl;}()
#define orz(x) [&]{cerr<<#x": "<<x<<endl;}()
#define orzarr(a,n) [&]{cerr<<#a": "; repeat(__,0,n)cerr<<(a)[__]<<" "; cerr<<endl;}()
#define orzeach(a) [&]{cerr<<#a": "; for(auto __:a)cerr<<__<<" "; cerr<<endl;}()
#define pause [&]{system("pause");}()
如果沒有萬能頭
#include<cstdio>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<ctime>
#include<cctype>
#include<iostream>
#include<algorithm>
//#include<chrono>
#include<vector>
#include<list>
#include<queue>
#include<string>
#include<set>
#include<map>
//#include<unordered_set>
//#include<unordered_map>
其他定義
#pragma GCC optimize(2) //(3),("Ofast")
#define lll __int128
#define inline __inline __attribute__((always_inline))
//struct name{bool operator()(const type &x,const type &y){return func(x,y);}}
#define vector basic_string
#define sortunique(a) ({sort(a.begin(),a.end()); a.erase(unique(a.begin(),a.end()),a.end());})
#define gets(s) (scanf("%[^\n]",s)+1)
template<typename T> T sqr(const T &x){return x*x;}
typedef long double lf;
template<typename A,typename B>void operator<<(A &a,B b){a.push_back(b);}
容器
平板電視紅黑樹
#include <ext/pb_ds/tree_policy.hpp>
#include <ext/pb_ds/assoc_container.hpp>
using namespace __gnu_pbds;
tree<pii,null_type,less<pii>,rb_tree_tag,tree_order_statistics_node_update> t; //紅黑樹
t.insert({x,i+1}); //----------------- 插入x,用獨特的正整數i+1標注(因為erase太辣雞)
t.erase(t.lower_bound({x,0})); //----- 刪除x(刪除單個元素)
t.order_of_key({x,0})+1; //----------- x的排名(小於x的元素個數+1)
t.find_by_order(x-1)->first; //------- 排名為x的元素(第x小的數)
prev(t.lower_bound({x,0}))->first; //- x的前驅(小於x且最大)
t.lower_bound({x+1,0})->first; //----- x的后繼(大於x且最小)
t.join(t2); //------------------------ 將t2並入t,t2清空,前提是取值范圍不相交
t.split(v,t2); //--------------------- 小於等於v的元素屬於t,其余的屬於t2
平板電視優先隊列
pairing_heap_tag
配對堆,應該是可並堆里最快的thin_heap_tag
斐波那契堆std::priority_queue
不合並就很快
#include<ext/pb_ds/priority_queue.hpp>
using namespace __gnu_pbds;
__gnu_pbds::priority_queue<int,less<int>,pairing_heap_tag> h; //大根堆
h.push(x); h.top(); h.pop();
h.join(h2); //將h2並入h,h2清空
rope
- (實現方式是塊狀鏈表還是平衡樹存疑)可能是可分裂平衡樹
#include <ext/rope>
using namespace __gnu_cxx;
rope<int> r; //塊狀鏈表
r.push_back(n);
r.insert(pos,n); //插入一個元素
r.erase(pos,len); //區間刪除
r.copy(pos,len,x); //區間賦值
r.substr(pos,len); //這是啥
r[pos] //只能訪問不能修改
rope<int> *his[N]; his[i]=new rope<int>(*his[i-1]); //據說O(1)拷貝,一行可持久化
其他語法
STL
a=move(b); //容器移動(a賦值為b,b清空)
priority_queue<int>(begin,end) //O(n)建堆
a.find(key) //set,map查找,沒找到返回a.end()
a.lower_bound(key) //set,map限制最小值
a.insert(b.begin(),b.end()); //set,map合並(時間復雜度極高)
complex<lf> c; complex<lf> c(1,2);//復數
c.real(),c.imag() //實部、虛部
bitset<32> b; //聲明一個32位的bitset
b[n]; b[n]=1; //訪問和修改
b.none(); //返回是否為空
b.count(); //返回1的個數
b.to_ullong(); b.to_string(); //轉換
unordered容器手寫hash
struct myhash{
typedef unsigned long long ull;
ull f(ull x)const{
x+=0x321354564536; //亂敲
x=(x^(x>>30))*0x3212132123; //亂敲
return x^(x>>31);
}
ull operator()(pii x)const{
static ull t=chrono::steady_clock::now().time_since_epoch().count();
return f(x.fi+t)^f(x.se+t*2);
}
};
unordered_set<pii,myhash> a;
cmath
fmod(x) //浮點取模
tgamma(x) //計算Γ(x)
atan2(x,y) //計算坐標(x,y)的極角
hypot(x,y) //計算sqrt(x^2+y^2)
scanf字符串正則化
scanf("%ns",str); //讀入n個字符
scanf("%[a-z]",str); //遇到非小寫字母停止
scanf("%[^0-9]",str); //遇到數字停止,^表示非
scanf("%*[a-z]"); //也是遇到非小寫字母停止,只不過不讀入字符串
神奇特性
- 命名空間rel_ops:之后只定義小於就能用其他所有次序關系符號
- raw strings:
R"(abc\n)"
相當於"abc\\n"
- 定義數字開頭的變量:
type operator ""_name(type number){/*...*/}
(之后1_name
即把1帶入上述函數中,參數類型只能是ull,llf,char,(const char *,size_t)
) - 高級宏:
__VA_ARGS__
是參數列表(對應...
),__LINE__
是當前行數,__FUNCTION__
是當前函數名,__COUNTER__
是宏展開次數-1 - 位域:
struct{int a:3;};
表示struct里a占3 bit,可以節省空間 - %n:
scanf,printf
中 %n 將讀入/輸出的字符個數寫入變量
Java
import java.util.*;
import java.math.BigInteger;
import java.math.BigDecimal;
public class Main{
static Scanner sc;
public static void main(String[] args){
sc=new Scanner(System.in);
}
}
- 編譯運行
java Main.java
- 編譯
javac Main.java
//生成Main.class - 運行
java Main
數據類型
int //4字節有符號
long //8字節有符號
double,boolean,char,String
final double PI=3.14; //final => (c++) const
var n=1; //var => (c++) auto
long
型常量結尾加L
,如1L
數組
int[] arr=new int[100]; //數組
int[][] arr=new int[10][10]; //二維數組
Array.sort(arr,l,r); //對arr[l..(r-1)]排序(import java.util.Arrays;)
輸出
System.out.print(x);
System.out.println();
System.out.println(x);
System.out.printf("%.2f\n",d); //格式化
輸入
import java.util.Scanner;
Scanner sc=new Scanner(System.in); //初始化
String s=sc.nextline(); //讀一行字符串
int n=sc.nextInt(); //讀整數
double d=sc.nextDouble(); //讀實數
sc.hasNext() //是否讀完
String
s1.equals(s2) //返回是否相等
s1.compareTo(s2) //s1>s2返回1,s1<s2返回-1,s1==s2返回0
s1.contains(s2) //返回是否包含子串s2
s1.indexOf(s2,begin=0) //返回子串位置
s1.substring(2,4) //返回子串,首末坐標[2,4)
s1.charAt(3) //返回第4個字符,就像c++的s1[3]
s1.length() //返回長度
s1+s2 //返回連接結果
String.format("%d",n) //返回格式化結果
Math
//不用import就能用下列函數
Math.{sqrt,sin,atan,abs,max,min,pow,exp,log,PI,E}
Random
import java.util.Random;
Random rnd=new Random(); //已經把時間戳作為了種子
rnd.nextInt();
rnd.nextInt(n); //[0,n)
BigInteger
import java.math.BigInteger;
BigInteger n=new BigInteger("0");
BigInteger[] arr=new BigInteger[10];
n1.intValue() //轉換為int
n1.longValue() //轉換
n1.doubleValue() //轉換
n1.add(n2) //加法
n1.subtract(n2) //減法
n1.multiply(n2) //乘法
n1.divide(n2) //除法
n1.mod(n2) //取模
BigInteger.valueOf(I) //int轉換為BigInteger
n1.compareTo(n2) //n1>n2返回1,n1<n2返回-1,n1==n2返回0
n1.abs()
n1.pow(I)
n1.toString(I) //返回I進制字符串
//運算時n2一定要轉換成BigInteger
BigDecimal
import java.math.BigDecimal;
n1.divide(n2,2,BigDecimal.ROUND_HALF_UP) //保留兩位(四舍五入)
//貌似沒有sqrt等操作,都得自己實現qwq
常規算法
算法基礎
- STL自帶算法
fill(begin,end,element); //填充
fill_n(begin,n,element); //填充
iota(begin,end,t); //遞增填充(賦值為t,t+1,t+2,...)
copy(a_begin,a_end,b_begin); //復制(注意會復制到b里)
reverse(begin,end); //翻轉
nth_element(begin,begin+k,end); //將第k+1小置於位置k,平均O(n)
binary_search(begin,end,key,[less]) //返回是否存在
upper_bound(begin,end,key,[less]) //返回限制最小值地址
lower_bound(begin,end,key,[less]) //返回嚴格限制最小值地址
merge(a_begin,a_end,b_begin,b_end,c_begin,[less]); //歸並a和b(結果存c)
inplace_merge(begin,begin+k,end); //歸並(原地保存)
next_permutation(begin,end); prev_permutation(begin,end); //允許多重集,返回不到底,使用方法 do{/*...*/}while(next_permutation(begin,end));
min_element(begin,end); max_element(begin,end); //返回最值的指針
for_each(begin,end,work); //每個元素進行work操作
auto it=back_inserter(a); //it=x表示往a.push_back(x)
- 進制轉換
strtol(str,0,base),strtoll //返回字符數組的base進制數
stol(s,0,base),stoll //返回字符串的base進制數
sscanf,sprintf //十進制
to_string(n) //十進制
- 隨機數
#include<random>
mt19937 rnd(time(0));
//巨佬定義:mt19937 rnd(chrono::high_resolution_clock::now().time_since_epoch().count());
cout<<rnd()<<endl; //范圍是unsigned int
rnd.max() //返回最大值
lf rndf(){return rnd()*1.0/rnd.max();}
lf rndf2(){return rndf()*2-1;}
- 手寫二分/三分
while(l<=r){
int mid=(l+r)/2; //l+(r-l)/2
if(ok(mid))l=m+1; else r=m-1; //小的值容易ok
}
//此時r是ok的右邊界
while(l<r){
int x=(l+r)/2,y=x+1; //l+(r-l)/2
if(work(x)<work(y))l=x+1; else r=y-1; //最大值
}
//此時l和r均為極值點
#define f(x) (-x*x+23*x)
const lf ph=(sqrt(5)-1)/2; //0.618
lf dfs(lf l,lf x,lf r,lf fx){
if(abs(l-r)<1e-9)return x;
lf y=l+ph*(r-l),fy=f(y);
if(fx<fy)return dfs(x,y,r,fy);
else return dfs(y,x,l,fx);
}
lf search(lf l,lf r){
lf x=r-ph*(r-l);
return dfs(l,x,r,f(x));
}
- 次大值
int m1=-inf,m2=-inf;
repeat(i,0,n){m1=max(m1,a[i]); if(m1>m2)swap(m1,m2);}
//m1即次大值
離散化
- 從小到大標號並賦值,\(O(n\log n)\),
是個好東西
void disc(int a[],int n){
vector<int> b(a,a+n);
sort(b.begin(),b.end());
b.erase(unique(b.begin(),b.end()),b.end());
repeat(i,0,n)
a[i]=lower_bound(b.begin(),b.end(),a[i])-b.begin(); //從0開始編號
}
void disc(int a[],int n,int d){ //把距離>d的拉近到d
vector<int> b(a,a+n);
sort(b.begin(),b.end());
b.erase(unique(b.begin(),b.end()),b.end());
vector<int> c(b.size()); c[0]=0; //從0開始編號
repeat(i,1,b.size())
c[i]=c[i-1]+min(d,b[i]-b[i-1]);
repeat(i,0,n)
a[i]=c[lower_bound(b.begin(),b.end(),a[i])-b.begin()];
}
01分數規划
- \(n\) 個物品,都有兩個屬性 \(a_i\) 和 \(b_i\),任意取 \(k\) 個物品使它們的 \(\dfrac {\sum a_j}{\sum b_j}\) 最大
- 解:二分答案
- \(m\) 是否滿足條件即判斷 \(\dfrac {\sum a_j}{\sum b_j}\ge m\),即 \(\sum(a_j-mb_j)\ge 0\)
- 因此計算 \(c_i=a_i-mb_i\) ,取前 \(k\) 個最大值看它們之和是否 \(\ge 0\)
- 如果限制條件是 \(\sum b_j\ge W\),則將 \(b_j\) 看成體積,\(a_j-mb_j\) 看成價值,轉換為背包dp
int n,k; lf a[N],b[N],c[N];
bool check(lf mid){
repeat(i,0,n)c[i]=a[i]-mid*b[i];
nth_element(c,c+k,c+n,greater<lf>());
lf sum=0; repeat(i,0,k)sum+=c[i];
return sum>=0;
}
lf solve(){
lf l=0,r=1;
while(r-l>1e-9){
lf mid=(l+r)/2;
if(check(mid))l=mid; else r=mid;
}
return l;
}
任務規划 | Livshits-Kladov定理
- 給出 \(n\) 個任務,第 \(i\) 個任務花費 \(t_i\) 時間,該任務開始之前等待 \(t\) 時間的代價是 \(f_i(t)\) 個數,求一個任務排列方式,最小化代價 \(\sum\limits_{i=1}^n f_j(\sum\limits_{j=1}^{i-1}t_i)\)
- Livshits-Kladov定理:當 \(f_i(t)\) 是一次函數 / 指數函數 / 相同的單增函數時,最優解可以用排序計算
- 一次函數:\(f_i(t)=c_it+d_i\),按 \(\dfrac {c_i}{t_i}\) 升序排列
- 指數函數:\(f_i(t)=c_ia^t+d_i\),按 \(\dfrac{1-a^{t_i}}{c_i}\) 升序排列
- 相同的單增函數:按 \(t_i\) 升序排序
分治
逆序數×二維偏序
- \(O(n\log n)\)
void merge(int l,int r){ //歸並排序
//對[l,r-1]的數排序
if(r-l<=1)return;
int mid=l+(r-l)/2;
merge(l,mid);
merge(mid,r);
int p=l,q=mid,s=l;
while(s<r){
if(p>=mid || (q<r && a[p]>a[q])){
t[s++]=a[q++];
ans+=mid-p; //統計逆序數
}
else
t[s++]=a[p++];
}
for(int i=l;i<r;++i)a[i]=t[i];
}
最大空矩陣 | 懸線法
- 求01矩陣中全是0的最大連續子矩陣(面積最大)\(O(nm)\)
- 此處障礙物是正方形。如果障礙只是一些整點,答案從 \(ab\) 變為 \((a+1)(b+1)\)
int n,m,a[N][N],l[N][N],r[N][N],u[N][N];
int getlm(){
int ans=0;
repeat(i,0,n)
repeat(k,0,m)
l[i][k]=r[i][k]=u[i][k]=(a[i][k]==0);
repeat(i,0,n){
repeat(k,1,m)
if(a[i][k]==0)
l[i][k]=l[i][k-1]+1; //可以向左延伸幾格
repeat_back(k,0,m-1)
if(a[i][k]==0)
r[i][k]=r[i][k+1]+1; //可以向右延伸幾格
repeat(k,0,m)
if(a[i][k]==0){
if(i!=0 && a[i-1][k]==0){
u[i][k]=u[i-1][k]+1; //可以向上延伸幾格
l[i][k]=min(l[i][k],l[i-1][k]);
r[i][k]=min(r[i][k],r[i-1][k]); //如果向上延伸u格,lr對應的修改
}
ans=max(ans,(l[i][k]+r[i][k]-1)*u[i][k]);
}
}
return ans;
}
搜索
舞蹈鏈×DLX
精確覆蓋
- 在01矩陣中找到某些行,它們兩兩不相交,且它們的並等於全集
- xy編號從 $1$ 開始!\(O(\exp)\),節點數 \(<5000\)
int n,m;
vector<int> rec; //dance后存所有選中的行的編號
struct DLX{
#define rep(i,i0,a) for(int i=a[i0];i!=i0;i=a[i])
int u[N],d[N],l[N],r[N],x[N],y[N]; //N=10010
int sz[N],h[N];
int top;
void init(){
top=m;
repeat(i,0,m+1){
sz[i]=0; u[i]=d[i]=i;
l[i]=i-1; r[i]=i+1;
}
l[0]=m; r[m]=0;
repeat(i,0,n+1)h[i]=-1;
rec.clear();
}
void add(int x0,int y0){
top++; sz[y0]++;
x[top]=x0; y[top]=y0;
u[top]=u[y0]; d[top]=y0;
u[d[top]]=d[u[top]]=top;
if(h[x0]<0)
h[x0]=l[top]=r[top]=top;
else{
l[top]=h[x0]; r[top]=r[h[x0]];
l[r[h[x0]]]=top; r[h[x0]]=top;
}
}
void remove(int c){
l[r[c]]=l[c]; r[l[c]]=r[c];
rep(i,c,d)rep(j,i,r){
u[d[j]]=u[j]; d[u[j]]=d[j];
sz[y[j]]--;
}
}
void resume(int c){
rep(i,c,d)rep(j,i,r){
u[d[j]]=d[u[j]]=j;
sz[y[j]]++;
}
l[r[c]]=r[l[c]]=c;
}
bool dance(int dep=1){ //返回是否可行
if(r[0]==0)return 1;
int c=r[0];
rep(i,0,r)if(sz[c]>sz[i])c=i;
remove(c);
rep(i,c,d){
rep(j,i,r)remove(y[j]);
if(dance(dep+1)){rec.push_back(x[i]);return 1;}
rep(j,i,l)resume(y[j]);
}
resume(c);
return 0;
}
}dlx;
重復覆蓋
- 在01矩陣中找到最少的行,它們的並等於全集
- xy編號還是從 $1$ 開始!\(O(\exp)\),節點數可能 \(<3000\)
struct DLX{
#define rep(i,d,s) for(node* i=s->d;i!=s;i=i->d)
struct node{
node *l,*r,*u,*d;
int x,y;
};
static const int M=2e5;
node pool[M],*h[M],*R[M],*pl;
int sz[M],vis[M],ans,clk;
void init(int n,int m){ //行和列
clk=0; ans=inf; pl=pool; ++m;
repeat(i,0,max(n,m)+1)
R[i]=sz[i]=0,vis[i]=-1;
repeat(i,0,m)
h[i]=new(pl++)node;
repeat(i,0,m){
h[i]->l=h[(i+m-1)%m];
h[i]->r=h[(i+1)%m];
h[i]->u=h[i]->d=h[i];
h[i]->y=i;
}
}
void link(int x,int y){
sz[y]++;
auto p=new(pl++)node;
p->x=x; p->y=y;
p->u=h[y]->u; p->d=h[y];
p->d->u=p->u->d=p;
if(!R[x])R[x]=p->l=p->r=p;
else{
p->l=R[x]; p->r=R[x]->r;
p->l->r=p->r->l=p;
}
}
void remove(node* p){
rep(i,d,p)i->l->r=i->r,i->r->l=i->l;
}
void resume(node* p){
rep(i,u,p)i->l->r=i->r->l=i;
}
int eval(){
++clk; int ret=0;
rep(i,r,h[0])
if(vis[i->y]!=clk){
++ret;
vis[i->y]=clk;
rep(j,d,i)rep(k,r,j)vis[k->y]=clk;
}
return ret;
}
void dfs(int d){
if(h[0]->r==h[0]){ans=min(ans,d); return;}
if(eval()+d>=ans)return;
node* c; int m=inf;
rep(i,r,h[0])
if(sz[i->y]<m){m=sz[i->y]; c=i;}
rep(i,d,c){
remove(i); rep(j,r,i)remove(j);
dfs(d+1);
rep(j,l,i)resume(j); resume(i);
}
}
int solve(){ //返回最優解
ans=inf; dfs(0); return ans;
}
}dlx;
啟發式算法
A-star
- 定義 \(g(v)\) 是 \(s\) 到 \(v\) 的實際代價,\(h(v)\) 是 \(v\) 到 \(t\) 的估計代價
- 定義估價函數 \(f(v)=g(v)+h(v)\)
- 每次從堆里取出 \(f(v)\) 最小的點進行更新
- 如果滿足 \(h(v_1)+w(v_2,v_1)\ge h(v_2)\) (存疑)則不需要重復更新同一點,可以用set標記,已標記的不入堆
模擬退火
- 以當前狀態 \(X\) 為中心,半徑為溫度 \(T\) 的圓(或球)內選一個新狀態 \(Y\)
- 計算 \(D=E(Y)-E(X)\) 新狀態勢能減去當前狀態勢能
- 如果 \(D<0\) 則狀態轉移(勢能 \(E\) 越小越優)
- 否則狀態轉移的概率是 \(\exp(-\dfrac{KD}{T})\)(Metropolis接受准則,
學不會) - 最后溫度乘以降溫系數,返回第一步
- 需要調 $3$ 個參數:初始溫度,終止溫度,降溫系數(?)
注意點:
- 讓運行時間在TLE邊緣試探
- 多跑幾次退火
- 多交幾次(注意風險)
- 可以先不用某准則,輸出中間過程后再調參
lf rndf(){return rnd()*1.0/rnd.max();}
vec rndvec(){return vec(rndf()*2-1,rndf()*2-1);}
//lf E(vec); //計算勢能
struct state{ //表示一個狀態
vec v; lf e; //位置和勢能
state(vec v=vec()):v(v),e(E(v)){}
operator lf(){return e;}
};
state getstate(){
state X; lf T=1000;
auto work=[&](){
state Y=X.v+rndvec()*T;
if(Y<X /*|| rndf()<exp(-K*(Y.e-X.e)/T)*/){X=Y; return 1;}
return 0;
};
while(T>1e-9){
if(work()){work(); work(); T*=1.1;}
T*=0.99992;
}
return X;
}
void solve(){
state X;
repeat(i,0,6){
state Y=getstate();
if(X>Y)X=Y;
}
printf("%.10f\n",lf(X));
}
動態規划
多重背包
- 二進制版,\(O(nV\log num)\),\(V\) 是總容量
int n,V; ll dp[N];
void push(int val,int v,int c){ //處理物品(價值=val,體積=v,個數=c)
for(int b=1;c;c-=b,b=min(b*2,c)){
ll dv=b*v,dval=b*val;
repeat_back(j,dv,V+1)
dp[j]=max(dp[j],dp[j-dv]+dval);
}
}
//初始化fill(dp,dp+V+1,0),結果是dp[V]
- 單調隊列版,\(O(nV)\),\(V\) 是總容量
int n,V; ll dp[N];
void push(int val,int v,int c){ //處理物品(價值=val,體積=v,個數=c)
static deque< pair<int,ll> > q; //單調隊列,fi是位置,se是價值
if(v==0){
repeat(i,0,V+1)dp[i]+=val*c;
return;
}
c=min(c,V/v);
repeat(d,0,v){
q.clear();
repeat(j,0,(V-d)/v+1){
ll t=dp[d+j*v]-j*val;
while(!q.empty() && t>=q.back().se)
q.pop_back();
q.push_back({j,t});
while(q.front().fi<j-c)
q.pop_front();
dp[d+j*v]=max(dp[d+j*v],q.front().se+j*val);
}
}
}
//初始化fill(dp,dp+V+1,0),結果是dp[V]
最長不降子序列×LIS
- 二分查找優化,\(O(n\log n)\)
const int inf=1e9;
repeat(i,0,n+1)dp[i]=inf; //初始化為inf
repeat(i,0,n)
*lower_bound(dp,dp+n,a[i])=a[i];
return lower_bound(dp,dp+n,inf)-dp;
數位dp
- 記憶化搜索,dfs的參數lim表示是否被限制,lz表示當前位的前一位是不是前導零
- 復雜度等於狀態數
- 如果每個方案貢獻不是1,dp可能要變成struct數組(cnt,sum,....)
ll dp[20][*][2],bit[20]; //這個[2]表示lz狀態,如果lz被使用了的話就需要記錄
ll dfs(int pos,ll *,bool lim=1,bool lz=1){
if(pos==-1)return *; //返回該狀態是否符合要求(0或1)
ll &x=dp[pos][*];
if(!lim && x!=-1)return x;
ll ans=0;
int maxi=lim?bit[pos]:9;
repeat(i,0,maxi+1){
...//狀態轉移
if(lz && i==0)...//可能要用lz,其他地方都不用
ans+=dfs(pos-1,*,
lim && i==maxi,
lz && i==0);
}
if(!lim)x=ans; //不限制的時候才做存儲
return ans;
}
ll solve(ll n){
int len=0;
while(n)bit[len++]=n%10,n/=10;
return dfs(len-1,*);
}
signed main(){
mst(dp,-1); //在很多時候dp值可以反復使用
ll t=read();
while(t--){
ll l=read(),r=read();
printf("%lld\n",solve(r)-solve(l-1));
}
return 0;
}
換根dp
- 兩次dfs
- 第一次求所有點所在子樹的答案 \(dp_v\),此時 \(dp_{rt}\) 是 \(rt\) 的最終答案
- 第二次將根轉移來算其他點的最終答案,回溯時復原即可
void dfs1(int x,int fa=-1){
for(auto p:a[x])
if(p!=fa){
dfs1(p,x);
dp[x]+=op(dp[p]);
}
}
void dfs2(int x,int fa=-1){
ans[x]=dp[x];
for(auto p:a[x])
if(p!=fa){
dp[x]-=op(dp[p]);
dp[p]+=op(dp[x]);
dfs2(p,x);
dp[p]-=op(dp[x]);
dp[x]+=op(dp[p]);
}
}
斜率優化
- 例:HDOJ3507
- \(dp_i=\min\limits_{j=0}^{i-1}[dp_j+(s_i-s_j)^2+M]\)
- 考慮 \(k<j<i\),\(j\) 決策優於 \(k\Leftrightarrow dp_j+(s_i-s_j)^2<dp_k+(s_i-s_k)^2\)
- 一通操作后 \(\dfrac{s_j^2+dp_j-s_k^2-dp_k}{2(s_j-s_k)}<s_i\),即尋找點集 \(\{(2s_j,s_j^2+dp_j)\}\) 的下凸包中斜率剛好 \(>s_i\) 的線段的左端點,作為決策點
四邊形優化
- \(dp(l,r)=\min\limits_{k=l}^{r-1}[dp(l,k)+dp(k+1,r)]+w(l,r)\)
- 其中 \(w(l,r)\) 滿足
- 區間包含單調性:任意 \(l \le l' \le r' \le r\) 有 \(w(l',r')\le w(l,r)\)
- 四邊形不等式:任意 \(a \le b \le c \le d\) 有 \(w(a,c)+w(b,d)\le w(a,d)+w(b,c)\)(若等號恆成立則滿足四邊形恆等式)
- 決策單調性:令 \(m(l,r)\) 為最優決策點(滿足 \(dp(l,r)=dp(l,m)+dp(m+1,r)+w(l,r)\)),則有 \(m(l,r-1) \le m(l,r) \le m(l+1,r)\),遍歷這個區間可以優化至 \(O(n^2)\)
repeat(i,0,n)dp[i][i]=0,m[i][i]=i;
repeat(len,2,n+1)
for(int l=0,r=len-1;r<n;l++,r++){
dp[l][r]=inf;
repeat(k,m[l][r-1],min(m[l+1][r]+1,r))
if(dp[l][r]>dp[l][k]+dp[k+1][r]+w(l,r)){
dp[l][r]=dp[l][k]+dp[k+1][r]+w(l,r);
m[l][r]=k;
}
}
- \(dp(i)=\min\limits_{k=1}^{i-1}w(k,i)\),\(w(l,r)\) 滿足四邊形不等式
- 決策單調性:令 \(m_i\) 為最優決策點(滿足 \(dp(i)=w(m,i)\)),則 \(m_{i-1}\le m_i\),因此可以分治優化成 \(O(n\log n)\)
計算幾何
struct of 向量
- rotate()返回逆時針旋轉后的點,left()返回朝左的單位向量
- trans()返回p沿a,b拉伸的結果,arctrans()返回p在坐標系<a,b>中的坐標
- 常量式寫法,不要另加變量,需要加變量就再搞個struct
- 直線類在半面交里,其中包含線段交點
struct vec{
lf x,y; vec(){} vec(lf x,lf y):x(x),y(y){}
vec operator-(const vec &b){return vec(x-b.x,y-b.y);}
vec operator+(const vec &b){return vec(x+b.x,y+b.y);}
vec operator*(lf k){return vec(k*x,k*y);}
lf len(){return hypot(x,y);}
lf sqr(){return x*x+y*y;}
vec trunc(lf k=1){return *this*(k/len());}
vec rotate(double th){lf c=cos(th),s=sin(th); return vec(x*c-y*s,x*s+y*c);}
vec left(){return vec(-y,x).trunc();}
lf theta(){return atan2(y,x);}
friend lf cross(vec a,vec b){return a.x*b.y-a.y*b.x;};
friend lf cross(vec a,vec b,vec c){return cross(a-c,b-c);}
friend lf dot(vec a,vec b){return a.x*b.x+a.y*b.y;}
friend vec trans(vec p,vec a,vec b){
swap(a.y,b.x);
return vec(dot(a,p),dot(b,p));
}
friend vec arctrans(vec p,vec a,vec b){
lf t=cross(a,b);
return vec(-cross(b,p)/t,cross(a,p)/t);
}
void output(){printf("%.12f %.12f\n",x,y);}
}a[N];
平面幾何基本操作
判斷兩條線段是否相交
- 快速排斥實驗:判斷線段所在矩形是否相交(用來減小常數,可省略)
- 跨立實驗:任一線段的兩端點在另一線段的兩側
bool judge(vec a,vec b,vec c,vec d){ //線段ab和線段cd
#define SJ(x) max(a.x,b.x)<min(c.x,d.x)\
|| max(c.x,d.x)<min(a.x,b.x)
if(SJ(x) || SJ(y))return 0;
#define SJ2(a,b,c,d) cross(a-b,a-c)*cross(a-b,a-d)<=0
return SJ2(a,b,c,d) && SJ2(c,d,a,b);
}
others of 平面幾何基本操作
點是否在線段上
bool onseg(vec p,vec a,vec b){
return (a.x-p.x)*(b.x-p.x)<eps
&& (a.y-p.y)*(b.y-p.y)<eps
&& abs(cross(a-b,a-p))<eps;
}
多邊形面積
lf area(vec a[],int n){
lf ans=0;
repeat(i,0,n)
ans+=cross(a[i],a[(i+1)%n]);
return abs(ans/2);
}
多邊形的面積質心
vec centre(vec a[],int n){
lf S=0; vec v=vec();
repeat(i,0,n){
vec &v1=a[i],&v2=a[(i+1)%n];
lf s=cross(v1,v2);
S+=s; v=v+(v1+v2)*s;
}
return v*(1/(3*S));
}
二維凸包
- 求上凸包,按坐標 \((x,y)\) 字典升序排序,從小到大加入棧,如果出現凹多邊形情況則出棧。下凸包反着來
- \(O(n\log n)\),排序是瓶頸
vector<vec> st;
void push(vec &v,int b){
while(st.size()>b
&& cross(*++st.rbegin(),st.back(),v)<=0) //會得到逆時針的凸包
st.pop_back();
st.push_back(v);
}
void convex(vec a[],int n){
st.clear();
sort(a,a+n);
repeat(i,0,n)push(a[i],1);
int b=st.size();
repeat_back(i,1,n-1)push(a[i],b); //repeat_back自動變成上凸包
}
旋轉卡殼
- 每次找到凸包每條邊的最遠點,基於二維凸包,\(O(n\log n)\)
lf calipers(vec a[],int n){
convex(a,n); //凸包算法
repeat(i,0,st.size())a[i]=st[i]; n=st.size();
lf ans=0; int p=1; a[n]=a[0];
repeat(i,0,n){
while(cross(a[p],a[i],a[i+1])<cross(a[p+1],a[i],a[i+1])) //必須逆時針凸包
p=(p+1)%n;
ans=max(ans,(a[p]-a[i]).len());
ans=max(ans,(a[p+1]-a[i]).len()); //這里求了直徑
}
return ans;
}
最大空矩形 | 掃描法
- 在范圍 \((0,0)\) 到 \((l,w)\) 內求面積最大的不覆蓋任何點的矩形面積,\(O(n^2)\),\(n\) 是點數
- 如果是
lf
就把vec
結構體內部、ans
、u
和d
的類型改一下
struct vec{
int x,y; //可能是lf
vec(int x,int y):x(x),y(y){}
};
vector<vec> a; //存放點
int l,w;
int ans=0;
void work(int i){
int u=w,d=0;
repeat(k,i+1,a.size())
if(a[k].y>d && a[k].y<u){
ans=max(ans,(a[k].x-a[i].x)*(u-d)); //更新ans
if(a[k].y==a[i].y)return; //可行性剪枝
(a[k].y>a[i].y?u:d)=a[k].y; //更新u和d
if((l-a[i].x)*(u-d)<=ans)return; //最優性剪枝
}
ans=max(ans,(l-a[i].x)*(u-d)); //撞牆更新ans
}
int query(){
a.push_back(vec(0,0));
a.push_back(vec(l,w)); //加兩個點方便處理
//小矩形的左邊靠着頂點的情況
sort(a.begin(),a.end(),[](vec a,vec b){return a.x<b.x;});
repeat(i,0,a.size())
work(i);
//小矩形的右邊靠着頂點的情況
repeat(i,0,a.size())a[i].x=l-a[i].x; //水平翻折
sort(a.begin(),a.end(),[](vec a,vec b){return a.x<b.x;});
repeat(i,0,a.size())
work(i);
//小矩形左右邊都不靠頂點的情況
sort(a.begin(),a.end(),[](vec a,vec b){return a.y<b.y;});
repeat(i,0,(int)a.size()-1)
ans=max(ans,(a[i+1].y-a[i].y)*l);
return ans;
}
平面最近點對 | 分治
- \(O(n\log n)\),可能有鍋
lf ans;
bool cmp_y(vec a,vec b){return a.y<b.y;}
void rec(int l,int r){ //左閉右開區間
#define upd(x,y) {ans=min(ans,(x-y).len());}
if(r-l<4){
repeat(i,l,r)
repeat(j,i+1,r)
upd(a[i],a[j]);
sort(a+l,a+r,cmp_y); //按y排序
return;
}
int m=(l+r)/2;
lf midx=a[m].x;
rec(l,m),rec(m,r);
static vec b[N];
merge(a+l,a+m,a+m,a+r,b+l,cmp_y); //逐漸按y排序
copy(b+l,b+r,a+l);
int t=0;
repeat(i,l,r)
if(abs(a[i].x-midx)<ans){
repeat_back(j,0,t){
if(a[i].y-b[i].y>ans)break;
upd(a[i],b[j]);
}
b[t++]=a[i];
}
}
lf nearest(){
ans=1e20;
sort(a,a+n); //按x排序
rec(0,n);
return ans;
}
最小圓覆蓋 | 隨機增量法×RIA
- eps可能要非常小。隨機化,均攤 \(O(n)\)
struct cir{ //圓(結構體)
vec v; lf r;
bool out(vec a){ //點a在圓外
return (v-a).len()>r+eps;
}
cir(vec a){v=a; r=0;}
cir(vec a,vec b){v=(a+b)*0.5; r=(v-a).len();}
cir(vec a,vec b,vec c){ //三個點的外接圓
b=b-a,c=c-a;
vec s=vec(b.sqr(),c.sqr())*0.5;
lf d=1/cross(b,c);
v=a+vec(s.x*c.y-s.y*b.y,s.y*b.x-s.x*c.x)*d;
r=(v-a).len();
}
};
cir RIA(vec a[],int n){
repeat_back(i,2,n)swap(a[rand()%i],a[i]); //random_shuffle(a,a+n);
cir c=cir(a[0]);
repeat(i,1,n)if(c.out(a[i])){
c=cir(a[i]);
repeat(j,0,i)if(c.out(a[j])){
c=cir(a[i],a[j]);
repeat(k,0,j)if(c.out(a[k]))
c=cir(a[i],a[j],a[k]);
}
}
return c;
}
半面交 | S&I算法
- 編號從 $0$ 開始,\(O(n\log n)\)
struct line{
vec p1,p2; lf th;
line(){}
line(vec p1,vec p2):p1(p1),p2(p2){
th=(p2-p1).theta();
}
bool contain(vec v){
return cross(v,p2,p1)<=eps;
}
vec PI(line b){ //point of intersection
lf t1=cross(p1,b.p2,b.p1);
lf t2=cross(p2,b.p2,b.p1);
return vec((t1*p2.x-t2*p1.x)/(t1-t2),(t1*p2.y-t2*p1.y)/(t1-t2));
}
};
vector<vec> ans; //ans: output, shows a convex hull
namespace half{
line a[N]; int n; //(a[],n): input, the final area will be the left of the lines
deque<line> q;
void solve(){
a[n++]=line(vec(inf,inf),vec(-inf,inf));
a[n++]=line(vec(-inf,inf),vec(-inf,-inf));
a[n++]=line(vec(-inf,-inf),vec(inf,-inf));
a[n++]=line(vec(inf,-inf),vec(inf,inf));
sort(a,a+n,[](line a,line b){
if(a.th<b.th-eps)return 1;
if(a.th<b.th+eps && b.contain(a.p1)==1)return 1;
return 0;
});
n=unique(a,a+n,[](line a,line b){return abs(a.th-b.th)<eps;})-a;
q.clear();
#define r q.rbegin()
repeat(i,0,n){
while(q.size()>1 && !a[i].contain(r[0].PI(r[1])))q.pop_back();
while(q.size()>1 && !a[i].contain(q[0].PI(q[1])))q.pop_front();
q.push_back(a[i]);
}
while(q.size()>1 && !q[0].contain(r[0].PI(r[1])))q.pop_back();
while(q.size()>1 && !r[0].contain(q[0].PI(q[1])))q.pop_front();
#undef r
ans.clear();
repeat(i,0,(int)q.size()-1)ans<<q[i].PI(q[i+1]);
ans<<q[0].PI(q.back());
}
}
Delaunay三角剖分
- 編號從 $0$ 開始,\(O(n\log n)\)
const lf eps=1e-8;
struct vec{
lf x,y; int id;
explicit vec(lf a=0,lf b=0,int c=-1):x(a),y(b),id(c){}
bool operator<(const vec &a)const{
return x<a.x || (abs(x-a.x)<eps && y<a.y);
}
bool operator==(const vec &a)const{
return abs(x-a.x)<eps && abs(y-a.y)<eps;
}
lf dist2(const vec &b){
return (x-b.x)*(x-b.x)+(y-b.y)*(y-b.y);
}
};
struct vec3D{
lf x,y,z;
explicit vec3D(lf a=0,lf b=0,lf c=0):x(a),y(b),z(c){}
vec3D(const vec &v){x=v.x,y=v.y,z=v.x*v.x+v.y*v.y;}
vec3D operator-(const vec3D &a)const{
return vec3D(x-a.x,y-a.y,z-a.z);
}
};
struct edge{
int id;
list<edge>::iterator c;
edge(int id=0){this->id=id;}
};
int cmp(lf v){return abs(v)>eps?(v>0?1:-1):0;}
lf cross(const vec &o,const vec &a,const vec &b){
return(a.x-o.x)*(b.y-o.y)-(a.y-o.y)*(b.x-o.x);
}
lf dot(const vec3D &a,const vec3D &b){return a.x*b.x+a.y*b.y+a.z*b.z;}
vec3D cross(const vec3D &a,const vec3D &b){
return vec3D(a.y*b.z-a.z*b.y,-a.x*b.z+a.z*b.x,a.x*b.y-a.y*b.x);
}
vector<pii> ans; //三角剖分結果
struct DT{ //使用方法:直接solve()
list<edge> a[N]; vec v[N]; int n;
void solve(int _n,vec _v[]){
n=_n;
copy(_v,_v+n,v);
sort(v,v+n);
divide(0,n-1);
ans.clear();
for(int i=0;i<n;i++){
for(auto p:a[i]){
if(p.id<i)continue;
ans.push_back({v[i].id,v[p.id].id});
}
}
}
int incircle(const vec &a,vec b,vec c,const vec &v){
if(cross(a,b,c)<0)swap(b,c);
vec3D a3(a),b3(b),c3(c),p3(v);
b3=b3-a3,c3=c3-a3,p3=p3-a3;
vec3D f=cross(b3,c3);
return cmp(dot(p3,f));
}
int intersection(const vec &a,const vec &b,const vec &c,const vec &d){
return cmp(cross(a,c,b))*cmp(cross(a,b,d))>0 &&
cmp(cross(c,a,d))*cmp(cross(c,d,b))>0;
}
void addedge(int u,int v){
a[u].push_front(edge(v));
a[v].push_front(edge(u));
a[u].begin()->c=a[v].begin();
a[v].begin()->c=a[u].begin();
}
void divide(int l,int r){
if(r-l<=2){
for(int i=l;i<=r;i++)
for(int j=i+1;j<=r;j++)addedge(i,j);
return;
}
int mid=(l+r)/2;
divide(l,mid); divide(mid+1,r);
int nowl=l,nowr=r;
for(int update=1;update;){
update=0;
vec vl=v[nowl],vr=v[nowr];
for(auto i:a[nowl]){
vec t=v[i.id];
lf v=cross(vr,vl,t);
if(cmp(v)>0 || (cmp(v)== 0 && vr.dist2(t)<vr.dist2(vl))){
nowl=i.id,update=1;
break;
}
}
if(update)continue;
for(auto i:a[nowr]){
vec t=v[i.id];
lf v=cross(vl,vr,t);
if(cmp(v)<0 || (cmp(v)== 0 && vl.dist2(t)<vl.dist2(vr))){
nowr=i.id,update=1;
break;
}
}
}
addedge(nowl,nowr);
while(1){
vec vl=v[nowl],vr=v[nowr];
int ch=-1,side=0;
for(auto i:a[nowl])
if(cmp(cross(vl,vr,v[i.id]))>0 && (ch==-1 || incircle(vl,vr,v[ch],v[i.id])<0)){
ch=i.id,side=-1;
}
for(auto i:a[nowr])
if(cmp(cross(vr,v[i.id],vl))>0 && (ch==-1 || incircle(vl,vr,v[ch],v[i.id])<0)){
ch=i.id,side=1;
}
if(ch==-1)break;
if(side==-1){
for(auto it=a[nowl].begin();it!=a[nowl].end();){
if(intersection(vl,v[it->id],vr,v[ch])){
a[it->id].erase(it->c);
a[nowl].erase(it++);
}
else it++;
}
nowl=ch;
addedge(nowl,nowr);
}
else{
for(auto it=a[nowr].begin();it!=a[nowr].end();){
if(intersection(vr,v[it->id],vl,v[ch])){
a[it->id].erase(it->c);
a[nowr].erase(it++);
}
else it++;
}
nowr=ch;
addedge(nowl,nowr);
}
}
}
}dt;
- 可以求最小生成樹
vec a[N]; DSU d;
vector<int> e[N]; //最小生成樹結果
void mst(){ //求最小生成樹
dt.solve(n,a);
sort(ans.begin(),ans.end(),[](const pii &A,const pii &B){
return a[A.fi].dist2(a[A.se])<a[B.fi].dist2(a[B.se]);
});
d.init(n);
for(auto i:ans)
if(d[i.fi]!=d[i.se]){
e[i.fi].push_back(i.se);
e[i.se].push_back(i.fi);
d[i.fi]=d[i.se];
}
}
struct of 三維向量
trunc(K)
返回K
在*this
上的投影向量rotate(P,L,th)
返回點P
繞軸(O,L)
旋轉th
弧度后的點rotate(P,L0,L1,th)
返回點P
繞軸(L0,L1)
旋轉th
弧度后的點
struct vec{
lf x,y,z; vec(){} vec(lf x,lf y,lf z):x(x),y(y),z(z){}
vec operator-(vec b){return vec(x-b.x,y-b.y,z-b.z);}
vec operator+(vec b){return vec(x+b.x,y+b.y,z+b.z);}
vec operator*(lf k){return vec(k*x,k*y,k*z);}
bool operator<(vec b)const{return make_tuple(x,y,z)<make_tuple(b.x,b.y,b.z);}
lf sqr(){return x*x+y*y+z*z;}
lf len(){return sqrt(x*x+y*y+z*z);}
vec trunc(lf k=1){return *this*(k/len());}
vec trunc(vec k){return *this*(dot(*this,k)/sqr());}
friend vec cross(vec a,vec b){
return vec(
a.y*b.z-a.z*b.y,
a.z*b.x-a.x*b.z,
a.x*b.y-a.y*b.x);
}
friend lf dot(vec a,vec b){return a.x*b.x+a.y*b.y+a.z*b.z;}
friend vec rotate(vec p,vec l,lf th){
struct four{
lf r; vec v;
four operator*(four b){
return {r*b.r-dot(v,b.v),v*b.r+b.v*r+cross(v,b.v)};
}
};
l=l.trunc();
four P={0,p};
four Q1={cos(th/2),l*sin(th/2)};
four Q2={cos(th/2),vec()-l*sin(th/2)};
return ((Q1*P)*Q2).v;
}
friend vec rotate(vec p,vec l0,vec l1,lf th){
return rotate(p-l0,l1-l0,th)+l0;
}
void output(){printf("%.12f %.12f %.12f\n",x,y,z);}
};
球面幾何
vec to_vec(lf lng,lf lat){ //lng經度,lat緯度,-90<lat<90
lng*=pi/180,lat*=pi/180;
lf z=sin(lat),m=cos(lat);
lf x=cos(lng)*m,y=sin(lng)*m;
return vec(x,y,z);
};
lf to_lng(vec v){return atan2(v.y,v.x)*180/pi;}
lf to_lat(vec v){return asin(v.z)*180/pi;}
lf angle(vec a,vec b){return acos(dot(a,b));}
三維凸包
- 將所有凸包上的面放入面集
f
中,其中face::p[i]
作為a
的下標,\(O(n^2)\)
const lf eps=1e-9;
struct vec{
lf x,y,z;
vec(lf x=0,lf y=0,lf z=0):x(x),y(y),z(z){};
vec operator-(vec b){return vec(x-b.x,y-b.y,z-b.z);}
lf len(){return sqrt(x*x+y*y+z*z);}
void shake(){ //微小擾動
x+=(rand()*1.0/RAND_MAX-0.5)*eps;
y+=(rand()*1.0/RAND_MAX-0.5)*eps;
z+=(rand()*1.0/RAND_MAX-0.5)*eps;
}
}a[N];
vec cross(vec a,vec b){
return vec(
a.y*b.z-a.z*b.y,
a.z*b.x-a.x*b.z,
a.x*b.y-a.y*b.x);
}
lf dot(vec a,vec b){return a.x*b.x+a.y*b.y+a.z*b.z;}
struct face{
int p[3];
vec normal(){ //法向量
return cross(a[p[1]]-a[p[0]],a[p[2]]-a[p[0]]);
}
lf area(){return normal().len()/2.0;}
};
vector<face> f;
bool see(face f,vec v){
return dot(v-a[f.p[0]],f.normal())>0;
}
void convex(vec a[],int n){
static vector<face> c;
static bool vis[N][N];
repeat(i,0,n)a[i].shake(); //防止四點共面
f.clear();
f.push_back((face){0,1,2});
f.push_back((face){0,2,1});
repeat(i,3,n){
c.clear();
repeat(j,0,f.size()){
bool t=see(f[j],a[i]);
if(!t) //加入背面
c.push_back(f[j]);
repeat(k,0,3){
int x=f[j].p[k],y=f[j].p[(k+1)%3];
vis[x][y]=t;
}
}
repeat(j,0,f.size())
repeat(k,0,3){
int x=f[j].p[k],y=f[j].p[(k+1)%3];
if(vis[x][y] && !vis[y][x]) //加入新面
c.push_back((face){x,y,i});
}
f.swap(c);
}
}
計算幾何雜項
正冪反演
- 給定反演中心 \(O\) 和反演半徑 \(R\)。若直線上的點 \(OPQ\) 滿足 \(|OP|\cdot|OQ|=R^2\),則 \(P\) 和 \(Q\) 互為反演點(令 \(R=1\) 也可)
- 不經過反演中心的圓的反演圖形是圓(計算時取圓上靠近/遠離中心的兩個點)
- 經過反演中心的圓的反演圖形是直線(計算時取遠離中心的點,做垂線)
others of 計算幾何雜項
曼哈頓、切比雪夫距離
- 曼:
mdist=|x1-x2|+|y1-y2|
- 切:
cdist=max(|x1-x2|,|y1-y2|)
- 轉換:
mdist((x,y),*)=cdist((x+y,x-y),**)
cdist((x,y),*)=mdist(((x+y)/2,(x-y)/2),**)
Pick定理
- 可以用Pick定理求多邊形內部整點個數,其中一條線段上的點數為 \(\gcd(|x_1-x_2|,|y_1-y_2|)+1\)
- 正方形點陣:
面積 = 內部點數 + 邊上點數 / 2 - 1
- 三角形點陣:
面積 = 2 * 內部點數 + 邊上點數 - 2
公式
- 三角形面積 \(S=\sqrt{P(P-a)(P-b)(P-c)}\),\(P\) 為半周長
- 斯特瓦爾特定理:\(BC\) 上一點 \(P\),有 \(AP=\sqrt{AB^2\cdot \dfrac{CP}{BC}+AC^2\cdot \dfrac{BP}{BC}-BP\cdot CP}\)
- 三角形內切圓半徑 \(r=\dfrac {2S} C\),外接圓半徑 \(R=\dfrac{a}{2\sin A}=\dfrac{abc}{4S}\)
- 四邊形有 \(a^2+b^2+c^2+d^2=D_1^2+D_2^2+4M^2\),\(D_1,D_2\) 為對角線,\(M\) 為對角線中點連線
- 圓內接四邊形有 \(ac+bd=D_1D_2\),\(S=\sqrt{(P-a)(P-b)(P-c)(P-d)}\),\(P\) 為半周長
- 棱台體積 \(V=\dfrac 13(S_1+S_2+\sqrt{S_1S_2})h\),\(S_1,S_2\) 為上下底面積
- 正棱台側面積 \(\dfrac 1 2(C_1+C_2)L\),\(C_1,C_2\) 為上下底周長,\(L\) 為斜高(上下底對應的平行邊的距離)
- 球全面積 \(S=4\pi r^2\),體積 \(V=\dfrac 43\pi r^3\),
- 球台(球在平行平面之間的部分)有 \(h=|\sqrt{r^2-r_1^2}\pm\sqrt{r^2-r_2^2}|\),側面積 \(S=2\pi r h\),體積 \(V=\dfrac{1}{6}\pi h[3(r_1^2+r_2^2)+h^2]\),\(r_1,r_2\) 為上下底面半徑
- 正三角形面積 \(S=\dfrac{\sqrt 3}{4}a^2\),正四面體面積 \(S=\dfrac{\sqrt 2}{12}a^3\)
- 四面體體積公式
lf sqr(lf x){return x*x;}
lf V(lf a,lf b,lf c,lf d,lf e,lf f){ //a,b,c共頂點
lf A=b*b+c*c-d*d;
lf B=a*a+c*c-e*e;
lf C=a*a+b*b-f*f;
return sqrt(4*sqr(a*b*c)-sqr(a*A)-sqr(b*B)-sqr(c*C)+A*B*C)/12;
}
數據結構
st表
普通st表
- 編號從 $0$ 開始,初始化 \(O(n\log n)\) 查詢 \(O(1)\)
struct ST{
#define logN 21
#define U(x,y) max(x,y)
ll a[N][logN];
void init(int n){
repeat(i,0,n)
a[i][0]=in[i];
repeat(k,1,logN)
repeat(i,0,n-(1<<k)+1)
a[i][k]=U(a[i][k-1],a[i+(1<<(k-1))][k-1]);
}
ll query(int l,int r){
int s=31-__builtin_clz(r-l+1);
return U(a[l][s],a[r-(1<<s)+1][s]);
}
}st;
二維st表
- 編號從 $0$ 開始,初始化 \(O(nm\log n\log m)\) 查詢 \(O(1)\)
struct ST{ //注意logN=log(N)+2
#define logN 9
#define U(x,y) max(x,y)
int f[N][N][logN][logN],log[N];
ST(){
log[1]=0;
repeat(i,2,N)
log[i]=log[i/2]+1;
}
void build(){
repeat(k,0,logN)
repeat(l,0,logN)
repeat(i,0,n-(1<<k)+1)
repeat(j,0,m-(1<<l)+1){
int &t=f[i][j][k][l];
if(k==0 && l==0)t=a[i][j];
else if(k)
t=U(f[i][j][k-1][l],f[i+(1<<(k-1))][j][k-1][l]);
else
t=U(f[i][j][k][l-1],f[i][j+(1<<(l-1))][k][l-1]);
}
}
int query(int x0,int y0,int x1,int y1){
int k=log[x1-x0+1],l=log[y1-y0+1];
return U(U(U(
f[x0][y0][k][l],
f[x1-(1<<k)+1][y0][k][l]),
f[x0][y1-(1<<l)+1][k][l]),
f[x1-(1<<k)+1][y1-(1<<l)+1][k][l]);
}
}st;
<補充>貓樹
- 編號從 $0$ 開始,初始化 \(O(n\log n)\) 查詢 \(O(1)\)
struct cat{
#define U(a,b) max(a,b) //查詢操作
#define a0 0 //查詢操作的零元
#define logN 21
vector<ll> a[logN];
vector<ll> v;
void init(){
repeat(i,0,logN)a[i].clear();
v.clear();
}
void push(ll in){
v.push_back(in);
int n=v.size()-1;
repeat(s,1,logN){
int len=1<<s; int l=n/len*len;
if(n%len==len/2-1){
repeat(i,0,len)a[s].push_back(a0);
repeat_back(i,0,len/2)a[s][l+i]=U(a[s][l+i+1],v[l+i]);
}
if(n%len>=len/2)
a[s][n]=(U(a[s][n-1],v[n]));
}
}
ll query(int l,int r){ //區間查詢
if(l==r)return v[l];
int s=32-__builtin_clz(l^r);
return U(a[s][l],a[s][r]);
}
}tr;
單調隊列
- 求所有長度為k的區間中的最大值,線性復雜度
struct MQ{ //查詢就用mq.q.front().first
deque<pii> q; //first:保存的最大值; second:時間戳
void init(){q.clear();}
void push(int x,int k){
static int T=0; T++;
while(!q.empty() && q.back().fi<=x) //max
q.pop_back();
q.push_back({x,T});
while(!q.empty() && q.front().se<=T-k)
q.pop_front();
}
void work(function<int&(int)> a,int n,int k){ //原地保存,編號從0開始
init();
repeat(i,0,n){
push(a(i),k);
if(i+1>=k)a(i+1-k)=q.front().fi;
}
}
void work(int a[][N],int n,int m,int k){ //原地保存,編號從0開始
repeat(i,0,n){
init();
repeat(j,0,m){
push(a[i][j],k);
if(j+1>=k)a[i][j+1-k]=q.front().fi;
}
}
m-=k-1;
repeat(j,0,m){
init();
repeat(i,0,n){
push(a[i][j],k);
if(i+1>=k)a[i+1-k][j]=q.front().fi;
}
}
}
}mq;
//求n*m矩陣中所有k*k連續子矩陣最大值之和 //編號從1開始
repeat(i,1,n+1)
mq.work([&](int x)->int&{return a[i][x+1];},m,k);
repeat(j,1,m-k+2)
mq.work([&](int x)->int&{return a[x+1][j];},n,k);
ll ans=0; repeat(i,1,n-k+2)repeat(j,1,m-k+2)ans+=a[i][j];
//或者
mq.work((int(*)[N])&(a[1][1]),n,m,k);
樹狀數組
普通樹狀數組
- 單點+區間,修改查詢 \(O(\log n)\)
#define lb(x) (x&(-x))
struct BIT{
ll t[N]; //一倍內存吧
void init(){
mst(t,0);
}
void add(ll x,ll k){ //位置x加上k
//x++;
for(;x<N;x+=lb(x))
t[x]+=k;
}
ll sum(ll x){ //求[1,x]的和 //[0,x]
//x++;
ll ans=0;
for(;x!=0;x-=lb(x))
ans+=t[x];
return ans;
}
}bit;
- 大佬的第 \(k\) 小(權值樹狀數組)
int findkth(int k){
int ans=0,cnt=0;
for (int i=20;i>=0;--i){
ans+=1<<i;
if (ans>=n || cnt+t[ans]>=k)ans-=1<<i;
else cnt+=t[ans];
}
return ans+1;
}
超級樹狀數組
- 基於樹狀數組,基本只允許加法,區間+區間,\(O(\log n)\)
struct SPBIT{
BIT a,a1;
void init(){a.init();a1.init();}
void add(ll x,ll y,ll k){
a.add(x,k);
a.add(y+1,-k);
a1.add(x,k*(x-1));
a1.add(y+1,-k*y);
}
ll sum(ll x,ll y){
return y*a.sum(y)-(x-1)*a.sum(x-1)-(a1.sum(y)-a1.sum(x-1));
}
}spbit;
二維超級樹狀數組
- 修改查詢 \(O(\log n\cdot\log m)\)
int n,m;
#define lb(x) (x&(-x))
struct BIT{
ll t[N][N]; //一倍內存吧
void init(){
mst(t,0);
}
void add(int x,int y,ll k){ //位置(x,y)加上k
//x++,y++; //如果要從0開始編號
for(int i=x;i<=n;i+=lb(i))
for(int j=y;j<=m;j+=lb(j))
t[i][j]+=k;
}
ll sum(int x,int y){ //求(1..x,1..y)的和
//x++,y++; //如果要從0開始編號
ll ans=0;
for(int i=x;i!=0;i-=lb(i))
for(int j=y;j!=0;j-=lb(j))
ans+=t[i][j];
return ans;
}
};
struct SPBIT{
BIT a,ax,ay,axy;
void add(int x,int y,int k){
a.add(x,y,k);
ax.add(x,y,k*x);
ay.add(x,y,k*y);
axy.add(x,y,k*x*y);
}
ll sum(int x,int y){
return a.sum(x,y)*(x*y+x+y+1)
-ax.sum(x,y)*(y+1)
-ay.sum(x,y)*(x+1)
+axy.sum(x,y);
}
void add(int x0,int y0,int x1,int y1,int k){ //區間修改
add(x0,y0,k);
add(x0,y1+1,-k);
add(x1+1,y0,-k);
add(x1+1,y1+1,k);
}
ll sum(int x0,int y0,int x1,int y1){ //區間查詢
return sum(x1,y1)
-sum(x0-1,y1)
-sum(x1,y0-1)
+sum(x0-1,y0-1);
}
}spbit;
線段樹
- 基本上適用於所有(線段樹能實現的)區間+區間
- 我刪了修改運算的零元,加了偷懶狀態(state),
終於能支持賦值操作.jpg
struct seg{ //初始化init()修改查詢tr->sth()
#define U(x,y) (x+y) //查詢運算
#define a0 0 //查詢運算的零元
void toz(ll x){z+=x,state=1;} //加載到懶標記
void toa(){a+=z*(r-l+1),z=0,state=0;} //懶標記加載到數據(z別忘了清空)
ll a,z; bool state; //數據,懶標記,是否偷了懶
int l,r; seg *lc,*rc;
void init(int,int);
void up(){a=U(lc->a,rc->a);}
void down(){
if(!state)return;
if(l<r){lc->toz(z); rc->toz(z);}
toa();
}
void update(int x,int y,ll k){
x=max(x,l); y=min(y,r); if(x>y){down();return;}
if(x==l && y==r){toz(k); down(); return;}
down();
lc->update(x,y,k);
rc->update(x,y,k);
up();
}
ll query(int x,int y){
x=max(x,l); y=min(y,r); if(x>y)return a0;
down();
if(x==l && y==r)return a;
return U(lc->query(x,y),rc->query(x,y));
}
}tr[N*2],*pl;
void seg::init(int _l,int _r){
l=_l,r=_r; state=0;
if(l==r){a=in[l]; return;}
int m=(l+r)>>1;
lc=++pl; lc->init(l,m);
rc=++pl; rc->init(m+1,r);
up();
}
void init(int l,int r){
pl=tr; tr->init(l,r);
}
<補充>zkw線段樹
- 單點+區間,編號從0開始,建樹 \(O(n)\) 修改查詢 \(O(\log n)\)
- 代碼量和常數都和樹狀數組差不多
struct seg{
#define U(a,b) max(a,b) //查詢操作
ll a0=0; //查詢操作的零元
int n; ll a[1024*1024*4*2]; //內存等於2^k且大於等於兩倍inn
void init(int inn){ //建樹
for(n=1;n<inn;n<<=1); repeat(i,inn,n)in[i]=a0;
repeat(i,0,n)a[n+i]=in[i];
repeat_back(i,1,n)up(i);
}
void up(int x){
a[x]=U(a[x<<1],a[(x<<1)^1]);
}
void update(int x,ll k){ //位置x加上k
a[x+=n]+=k; //也可以賦值等操作
while(x>>=1)up(x);
}
ll query(int l,int r){ //區間查詢
ll ans=a0;
for(l+=n-1,r+=n+1;l^r^1;l>>=1,r>>=1){
if(~l & 1)ans=U(ans,a[l^1]); //l^1其實是l+1
if(r & 1)ans=U(ans,a[r^1]); //r^1其實是r-1
}
return ans;
}
}tr;
<補充>李超線段樹
- 支持插入線段、查詢所有線段與 \(x=x_0\) 交點最高的那條線段
- 修改 \(O(\log^2n)\),查詢 \(O(\log n)\)
int funx; //這是y()的參數
struct func{
lf k,b; int id;
lf y()const{return k*funx+b;} //funx點處的高度
bool operator<(const func &b)const{
return make_pair(y(),-id)<make_pair(b.y(),-b.id);
}
};
struct seg{ //初始化init()更新update()查詢query(),func::y()是高度
func a;
int l,r;
seg *ch[2];
void init(int,int);
void push(func d){
funx=(l+r)/2;
if(a<d)swap(a,d); //這個小於要用funx
if(l==r)return;
ch[d.k>a.k]->push(d);
}
void update(int x,int y,const func &d){ //更新[x,y]區間
x=max(x,l); y=min(y,r); if(x>y)return;
if(x==l && y==r)push(d);
else{
ch[0]->update(x,y,d);
ch[1]->update(x,y,d);
}
}
const func &query(int x){ //詢問
funx=x;
if(l==r)return a;
const func &b=ch[(l+r)/2<x]->query(x);
return max(a,b); //這個max要用funx
}
}tr[N*2],*pl;
void seg::init(int _l,int _r){
l=_l,r=_r; a={0,-inf,-1}; //可能隨題意改變
if(l==r)return;
int m=(l+r)/2;
(ch[0]=++pl)->init(l,m);
(ch[1]=++pl)->init(m+1,r);
}
void init(int l,int r){
pl=tr;
tr->init(l,r);
}
void add(int x0,int y0,int x1,int y1){ //線段處理並更新
if(x0>x1)swap(x0,x1),swap(y0,y1);
lf k,b;
if(x0==x1)k=0,b=max(y0,y1);
else{
k=lf(y1-y0)/(x1-x0);
b=y0-k*x0;
}
id++;
tr->update(x0,x1,{k,b,id});
}
並查集
- 合並查找 \(O(α(n))\),可視為 \(O(1)\)
普通並查集
- 精簡版,只有路徑壓縮
struct DSU{ //合並:d[x]=d[y],查找:d[x]==d[y]
int a[N];
void init(int n){iota(a,a+n+1,0);}
int fa(int x){
return a[x]==x?x:a[x]=fa(a[x]);
}
int &operator[](int x){
return a[fa(x)];
}
}d;
- 普通版,路徑壓縮+啟發式合並
struct DSU{
int a[10010],sz[10010];
void init(int n){
iota(a,a+n+1,0);
fill(sz,sz+n+1,1);
}
int fa(int x){
return a[x]==x?x:a[x]=fa(a[x]);
}
bool query(int x,int y){ //查找
return fa(x)==fa(y);
}
void join(int x,int y){ //合並
x=fa(x),y=fa(y);
if(x==y)return;
if(sz[x]>sz[y])swap(x,y);
a[x]=y;
sz[y]+=sz[x];
}
}d;
<補充>種類並查集
struct DSU{
int a[50010],r[50010];
void init(int n){
repeat(i,0,n+1)a[i]=i,r[i]=0;
}
int plus(int a,int b){ //關系a+關系b,類似向量相加
if(a==b)return -a;
return a+b;
}
int inv(int a){ //關系a的逆
return -a;
}
int fa(int x){ //返回根節點
if(a[x]==x)return x;
int f=a[x],ff=fa(f);
r[x]=plus(r[x],r[f]);
return a[x]=ff;
}
bool query(int x,int y){ //是否存在關系
return fa(x)==fa(y);
}
int getr(int x,int y){ //查找關系
return plus(r[x],inv(r[y]));
}
void join(int x,int y,int r2){ //按r2關系合並
r2=plus(plus(inv(r[x]),r2),r[y]);
x=fa(x),y=fa(y);
a[x]=y,r[x]=r2;
}
}d;
左偏樹
- 萬年不用,\(O(?)\)
如果沒有特殊要求一律平板電視
struct leftist{ //編號從1開始,因為空的左右兒子會指向0
#define lc LC[x]
#define rc RC[x]
vector<int> val,dis,exist,dsu,LC,RC;
void init(){add(0);dis[0]=-1;}
void add(int v){
int t=val.size();
val.pb(v);
dis.pb(0);
exist.pb(1);
dsu.pb(t);
LC.pb(0);
RC.pb(0);
}
int top(int x){
return dsu[x]==x?x:dsu[x]=top(dsu[x]);
}
void join(int x,int y){
if(exist[x] && exist[y] && top(x)!=top(y))
merge(top(x),top(y));
}
int merge(int x,int y){
if(!x || !y)return x+y;
if(val[x]<val[y]) //大根堆
swap(x,y);
rc=merge(rc,y);
if(dis[lc]<dis[rc])
swap(lc,rc);
dsu[lc]=dsu[rc]=dsu[x]=x;
dis[x]=dis[rc]+1;
return x;
}
void pop(int x){
x=top(x);
exist[x]=0;
dsu[lc]=lc;
dsu[rc]=rc;
dsu[x]=merge(lc,rc); //指向x的dsu也能正確指向top
}
#undef lc
#undef rc
}lt;
//添加元素lt.add(v),位置是lt.val.size()-1
//是否未被pop:lt.exist(x)
//合並:lt.join(x,y)
//堆頂:lt.val[lt.top(x)]
//彈出:lt.pop(x)
珂朵莉樹×老司機樹
- 珂朵莉數以區間形式存儲數據,
非常暴力,適用於有區間賦值操作的題 - 均攤 \(O(n\log\log n)\),但是
很可能被卡
struct ODT{
struct node{
int l,r;
mutable int v; //強制可修改
bool operator<(const node &b)const{return l<b.l;}
};
set<node> a;
void init(){ //初始化
a.clear();
a.insert({-inf,inf,0});
}
set<node>::iterator split(int x){ //分裂區間
auto it=--a.upper_bound({x,0,0});
if(it->l==x)return it;
int l=it->l,r=it->r,v=it->v;
a.erase(it);
a.insert({l,x-1,v});
return a.insert({x,r,v}).first;
}
void assign(int l,int r,int v){ //區間賦值
auto y=split(r+1),x=split(l);
a.erase(x,y);
a.insert({l,r,v});
}
int sum(int l,int r){ //操作示例:區間求和
auto y=split(r+1),x=split(l);
int ans=0;
for(auto i=x;i!=y;i++){
ans+=(i->r-i->l+1)*i->v;
}
return ans;
}
}odt;
K-D tree
- 例題:luogu P4148
- 支持在線在(x,y)處插入值、查詢二維區間和
- 插入、查詢 \(O(\log n)\)
struct node{
int x,y,v;
}s[N];
bool cmp1(int a,int b){return s[a].x<s[b].x;}
bool cmp2(int a,int b){return s[a].y<s[b].y;}
struct kdtree{
int rt,cur; //rt根節點
int d[N],sz[N],lc[N],rc[N]; //d=1豎着砍,sz子樹大小
int L[N],R[N],D[N],U[N]; //該子樹的界線
int sum[N]; //維護的二維區間信息(二維區間和)
int g[N],gt;
void up(int x){ //更新信息
sz[x]=sz[lc[x]]+sz[rc[x]]+1;
sum[x]=sum[lc[x]]+sum[rc[x]]+s[x].v;
L[x]=R[x]=s[x].x;
D[x]=U[x]=s[x].y;
if(lc[x]){
L[x]=min(L[x],L[lc[x]]);
R[x]=max(R[x],R[lc[x]]);
D[x]=min(D[x],D[lc[x]]);
U[x]=max(U[x],U[lc[x]]);
}
if(rc[x]){
L[x]=min(L[x],L[rc[x]]);
R[x]=max(R[x],R[rc[x]]);
D[x]=min(D[x],D[rc[x]]);
U[x]=max(U[x],U[rc[x]]);
}
}
int build(int l,int r){ //以序列g[l..r]為模板重建樹,返回根節點
if(l>r)return 0;
int mid=(l+r)>>1;
lf ax=0,ay=0,sx=0,sy=0;
for(int i=l;i<=r;i++)ax+=s[g[i]].x,ay+=s[g[i]].y;
ax/=(r-l+1);
ay/=(r-l+1);
for(int i=l;i<=r;i++){
sx+=(ax-s[g[i]].x)*(ax-s[g[i]].x);
sy+=(ay-s[g[i]].y)*(ay-s[g[i]].y);
}
if(sx>sy)
nth_element(g+l,g+mid,g+r+1,cmp1),d[g[mid]]=1;
else
nth_element(g+l,g+mid,g+r+1,cmp2),d[g[mid]]=2;
lc[g[mid]]=build(l,mid-1);
rc[g[mid]]=build(mid+1,r);
up(g[mid]);
return g[mid];
}
void pia(int x){ //將樹還原成序列g
if(!x)return;
pia(lc[x]);
g[++gt]=x;
pia(rc[x]);
}
void ins(int &x,int v){
if(!x){
x=v;
up(x);
return;
}
#define ch(f) (f?rc:lc)
if(d[x]==1)
ins(ch(s[v].x>s[x].x)[x],v);
else
ins(ch(s[v].y>s[x].y)[x],v);
up(x);
if(0.725*sz[x]<=max(sz[lc[x]],sz[rc[x]])){
gt=0;
pia(x);
x=build(1,gt);
}
}
void insert(int x,int y,int v){ //在(x,y)處插入元素
cur++;
s[cur]={x,y,v};
ins(rt,cur);
}
int x1,x2,y1,y2;
int qry(int x){
if(!x || x2<L[x] || x1>R[x] || y2<D[x] || y1>U[x])return 0;
if(x1<=L[x] && R[x]<=x2 && y1<=D[x] && U[x]<=y2)return sum[x];
int ret=0;
if(x1<=s[x].x && s[x].x<=x2 && y1<=s[x].y && s[x].y<=y2)
ret+=s[x].v;
return qry(lc[x])+qry(rc[x])+ret;
}
int query(int _x1,int _x2,int _y1,int _y2){ //查詢[x1,x2]×[y1,y2]的區間和
x1=_x1; x2=_x2; y1=_y1; y2=_y2;
return qry(rt);
}
void init(){
rt=cur=0;
}
}tr;
莫隊
- 離線(甚至在線)處理區間問題,
猛得一批
普通莫隊
- 移動指針 \(l,r\) 來求所有區間的答案
- 塊大小為 \(\sqrt n\),\(O(n^{\tfrac 3 2})\)
struct node{
int l,r,id;
bool operator<(const node &b)const{
if(l/unit!=b.l/unit)return l<b.l; //按塊排序
if((l/unit)&1) //奇偶化排序
return r<b.r;
return r>b.r;
}
};
vector<node> query; //查詢區間
int unit,n,bkt[N],a[N],final_ans[N]; //bkt是桶
ll ans;
void update(int x,int d){
int &b=bkt[a[x]];
ans-=C(b,2); //操作示例
b+=d;
ans+=C(b,2); //操作示例
}
void solve(){ //final_ans[]即最終答案
fill(bkt,bkt+n+1,0);
unit=int(ceil(sqrt(n)));
sort(query.begin(),query.end());
int l=1,r=0; ans=0; //如果原數組a編號從1開始
for(auto i:query){
while(l<i.l)update(l++,-1);
while(l>i.l)update(--l,1);
while(r<i.r)update(++r,1);
while(r>i.r)update(r--,-1);
final_ans[i.id]=ans;
}
}
//repeat(i,0,m)query.push_back({read(),read(),i}); //輸入查詢區間
帶修莫隊
- 相比與普通莫隊,多了一個時間軸
- 塊大小為 \(\sqrt[3]{nt}\),\(O(\sqrt[3]{n^4t})\)
- 空缺
二叉搜索樹
不平衡的二叉搜索樹
- 左子樹所有結點 \(\le v <\) 右子樹所有節點,目前僅支持插入,查詢可以寫一個
map<int,TR *>
struct TR{
TR *ch[2],*fa; //ch[0]左兒子,ch[1]右兒子,fa父親,根的父親是inf
int v,dep; //v是結點索引,dep深度,根的深度是1
TR(TR *fa,int v,int dep):fa(fa),v(v),dep(dep){
mst(ch,0);
}
void insert(int v2){ //tr->insert(v2)插入結點
auto &c=ch[v2>v];
if(c==0)c=new TR(this,v2,dep+1);
else c->insert(v2);
}
}*tr=new TR(0,inf,0);
//inf是無效節點,用tr->ch[0]來訪問根節點
無旋treap
- 普通平衡樹按v分裂,文藝平衡樹按sz分裂
- insert,erase操作在普通平衡樹中,push_back,output(dfs)在文藝平衡樹中
- 普通平衡樹
struct treap{
struct node{
int pri,v,sz;
node *l,*r;
node(int _v){pri=rnd(); v=_v; l=r=0; sz=1;}
node(){}
friend int size(node *u){return u?u->sz:0;}
void up(){sz=1+size(l)+size(r);}
friend pair<node *,node *> split(node *u,int key){ //按v分裂
if(u==0)return {0,0};
if(key<u->v){
auto o=split(u->l,key);
u->l=o.se; u->up();
return {o.fi,u};
}
else{
auto o=split(u->r,key);
u->r=o.fi; u->up();
return {u,o.se};
}
}
friend node *merge(node *x,node *y){
if(x==0 || y==0)return max(x,y);
if(x->pri>y->pri){
x->r=merge(x->r,y); x->up();
return x;
}
else{
y->l=merge(x,y->l); y->up();
return y;
}
}
int find_by_order(int ord){
if(ord==size(l))return v;
if(ord<size(l))return l->find_by_order(ord);
else return r->find_by_order(ord-size(l)-1);
}
}pool[N],*pl,*rt;
void init(){
pl=pool;
rt=0;
}
void insert(int key){
auto o=split(rt,key);
*++pl=node(key);
o.fi=merge(o.fi,pl);
rt=merge(o.fi,o.se);
}
void erase_all(int key){
auto o=split(rt,key-1),s=split(o.se,key);
rt=merge(o.fi,s.se);
}
void erase_one(int key){
auto o=split(rt,key-1),s=split(o.se,key);
rt=merge(o.fi,merge(merge(s.fi->l,s.fi->r),s.se));
}
int order(int key){
auto o=split(rt,key-1);
int ans=size(o.fi);
rt=merge(o.fi,o.se);
return ans;
}
int operator[](int x){
return rt->find_by_order(x);
}
int lower_bound(int key){
auto o=split(rt,key-1);
int ans=o.se->find_by_order(0);
rt=merge(o.fi,o.se);
return ans;
}
int nxt(int key){return lower_bound(key+1);}
}tr;
//if(opt==1)tr.insert(x);
//if(opt==2)tr.erase_one(x);
//if(opt==3)cout<<tr.order(x)+1<<endl; //x的排名
//if(opt==4)cout<<tr[x-1]<<endl; //排名為x
//if(opt==5)cout<<tr[tr.order(x)-1]<<endl; //前驅
//if(opt==6)cout<<tr.nxt(x)<<endl; //后繼
- 文藝平衡樹,tag表示翻轉子樹(區間)
struct treap{
struct node{
int pri,v,sz,tag;
node *l,*r;
node(int _v){pri=(int)rnd(); v=_v; l=r=0; sz=1; tag=0;}
node(){}
friend int size(node *u){return u?u->sz:0;}
void up(){sz=1+size(l)+size(r);}
void down(){
if(tag){
swap(l,r);
if(l)l->tag^=1;
if(r)r->tag^=1;
tag=0;
}
}
friend pair<node *,node *> split(node *u,int key){ //按sz分裂
if(u==0)return {0,0};
u->down();
if(key<size(u->l)){
auto o=split(u->l,key);
u->l=o.se; u->up();
return {o.fi,u};
}
else{
auto o=split(u->r,key-size(u->l)-1);
u->r=o.fi; u->up();
return {u,o.se};
}
}
friend node *merge(node *x,node *y){
if(x==0 || y==0)return max(x,y);
if(x->pri>y->pri){
x->down();
x->r=merge(x->r,y); x->up();
return x;
}
else{
y->down();
y->l=merge(x,y->l); y->up();
return y;
}
}
}pool[N],*pl,*rt;
void init(){
pl=pool;
rt=0;
}
void push_back(int v){
*++pl=node(v);
rt=merge(rt,pl);
}
void add_tag(int l,int r){ //編號從0開始
node *a,*b,*c;
tie(a,b)=split(rt,l-1);
tie(b,c)=split(b,r-l);
if(b)b->tag^=1;
rt=merge(a,merge(b,c));
}
void output(node *u){
if(u==0)return; u->down();
output(u->l); cout<<u->v<<' '; output(u->r);
}
}tr;
一些建議
雙頭優先隊列可以用multiset
支持插入、查詢中位數可以用雙堆
priority_queue<ll> h1; //大根堆
priority_queue< ll,vector<ll>,greater<ll> > h2; //小根堆
void insert(ll x){
#define maintain(h1,h2,b) {h1.push(x); if(h1.size()>h2.size()+b)h2.push(h1.top()),h1.pop();}
if(h1.empty() || h1.top()>x)maintain(h1,h2,1)
else maintain(h2,h1,0);
}
//h1.size()+h2.size()為奇數時h1.top()為中位數,偶數看題目定義
雙關鍵字堆可以用兩個multiset模擬
struct HEAP{
multiset<pii> a[2];
void init(){a[0].clear();a[1].clear();}
pii rev(pii x){return {x.second,x.first};}
void push(pii x){
a[0].insert(x);
a[1].insert(rev(x));
}
pii top(int p){
pii t=*--a[p].end();
return p?rev(t):t;
}
void pop(int p){
auto t=--a[p].end();
a[p^1].erase(a[p^1].lower_bound(rev(*t)));
a[p].erase(t);
}
};
高維前綴和
- 以二維為例,t是維數
- 法一 \(O(n^t2^t)\)
- 法二 \(O(n^tt)\)
//<1>
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
b[i][j]=b[i-1][j]+b[i][j-1]-b[i-1][j-1]+a[i][j];
//<2>
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
a[i][j]+=a[i][j-1];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
a[i][j]+=a[i-1][j];
一個01串,支持把某位置的1改成0,查詢某位置之后第一個1的位置,可以用並查集(刪除 d[x]=d[x+1]
,查詢 d[x]
)
手寫deque很可能比stl deque慢(吸氧時)
圖論
圖論的一些概念
- 基環圖:樹加一條邊
- 簡單圖:不含重邊和自環
- 完全圖:頂點兩兩相連的無向圖
- 競賽圖:頂點兩兩相連的有向圖
- 點u到v可達:有向圖中,存在u到v的路徑
- 點u和v聯通:無向圖中,存在u到v的路徑
- 生成子圖:點集和原圖相同
- 導出子圖/誘導子圖:選取一個點集,盡可能多加邊
- 正則圖:所有點的度均相同的無向圖
- 強正則圖:與任意兩個相鄰的點相鄰的點數相同,與任意兩個不相鄰的點相鄰的點數相同的正則圖
- 強正則圖的點數 \(v\),度 \(k\),相鄰的點的共度 \(\lambda\),不相鄰的點的共度 \(\mu\) 有 \(k(k-1-\lambda)=\mu(v-1-k)\)
- 強正則圖的例子:所有完全圖、所有nk頂點滿n分圖
- 點割集:極小的,把圖分成多個聯通塊的點集
- 割點:自身就是點割集的點
- 邊割基:極小的,把圖分成多個聯通塊的邊集
- 橋:自身就是邊割集的邊
- 點聯通度:最小點割集的大小
- 邊聯通度:最小邊割集的大小
- Whitney定理:點聯通度≤邊聯通度≤最小度
- 最大團:最大完全子圖
- 最大獨立集:最多的兩兩不連接的頂點
- 最小染色數:相鄰的點不同色的最少色數
- 最小團覆蓋數:覆蓋整個圖的最少團數
- 最大獨立集即補圖最大團
- 最小染色數等於補圖最小團覆蓋數
- 哈密頓通路:通過所有頂點有且僅有一次的路徑,若存在則為半哈密頓圖/哈密頓圖
- 哈密頓回路:通過所有頂點有且僅有一次的回路,若存在則為哈密頓圖
- 完全圖 \(K_{2k+1}\) 的邊集可以划分為 \(k\) 個哈密頓回路
- 完全圖 \(K_{2k}\) 的邊集去掉 \(k\) 條互不相鄰的邊后可以划分為 \(k-1\) 個哈密頓回路
圖論基礎
前向星
struct edge{int to,w,nxt;}; //指向,權值,下一條邊
vector<edge> a;
int head[N];
void addedge(int x,int y,int w){
a.push_back({y,w,head[x]});
head[x]=a.size()-1;
}
void init(int n){ //初始化
a.clear();
fill(head,head+n,-1);
}
//for(int i=head[x];i!=-1;i=a[i].nxt) //遍歷x出發的邊(x,a[i].to)
拓撲排序×Toposort
- \(O(V+E)\)
vector<int> topo;
void toposort(int n){
static int deg[N]; fill(deg,deg+n,0);
static queue<int> q;
repeat(x,0,n)for(auto p:a[x])deg[p]++;
repeat(i,0,n)if(deg[i]==0)q.push(i);
while(!q.empty()){
int x=q.front(); q.pop(); topo.push_back(x);
for(auto p:a[x])if(--deg[p]==0)q.push(p);
}
}
歐拉路徑 歐拉回路
- 若存在則路徑為 \(dfs\) 退出序(最后的序列還要再反過來)(如果for從小到大,可以得到最小字典序)
- (不記錄點的 \(vis\),只記錄邊的 \(vis\))
dfs樹 bfs樹
- 無向圖dfs樹:樹邊、返祖邊
- 有向圖dfs樹:樹邊、返祖邊、橫叉邊、前向邊
- 無向圖bfs樹:樹邊、返祖邊、橫叉邊
- 空缺
最短路徑
Dijkstra
- 僅限正權,\(O(E\log E)\)
struct node{
int to; ll dis;
bool operator<(const node &b)const{
return dis>b.dis;
}
};
int n;
bool vis[N];
vector<node> a[N];
void dij(int s,ll dis[]){ //s是起點,dis是結果
fill(vis,vis+n+1,0);
fill(dis,dis+n+1,inf); dis[s]=0; //last[s]=-1;
static priority_queue<node> q; q.push({s,0});
while(!q.empty()){
int x=q.top().to; q.pop();
if(vis[x])continue; vis[x]=1;
for(auto i:a[x]){
int p=i.to;
if(dis[p]>dis[x]+i.dis){
dis[p]=dis[x]+i.dis;
q.push({p,dis[p]});
//last[p]=x; //last可以記錄最短路(倒着)
}
}
}
}
Floyd
- \(O(V^3)\)
repeat(k,0,n)
repeat(i,0,n)
repeat(j,0,n)
f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
- 補充:
bitset
優化(只考慮是否可達),\(O(V^3)\)
//bitset<N> g<N>;
repeat(i,0,n)
repeat(j,0,n)
if(g[j][i])
g[j]|=g[i];
SPFA
- SPFA搜索中,有一個點入隊 \(n+1\) 次即存在負環
- 編號從 $0$ 開始,\(O(VE)\)
int cnt[N]; bool vis[N]; ll h[N]; //h意思和dis差不多,但是Johnson里需要區分
int n;
struct node{int to; ll dis;};
vector<node> a[N];
bool spfa(int s){ //返回是否有負環(s為起點)
repeat(i,0,n+1)
cnt[i]=vis[i]=0,h[i]=inf;
h[s]=0; //last[s]=-1;
static deque<int> q; q.assign(1,s);
while(!q.empty()){
int x=q.front(); q.pop_front();
vis[x]=0;
for(auto i:a[x]){
int p=i.to;
if(h[p]>h[x]+i.dis){
h[p]=h[x]+i.dis;
//last[p]=x; //last可以記錄最短路(倒着)
if(vis[p])continue;
vis[p]=1;
q.push_back(p); //可以SLF優化
if(++cnt[p]>n)return 1;
}
}
}
return 0;
}
bool negcycle(){ //返回是否有負環
a[n].clear();
repeat(i,0,n)
a[n].push_back({i,0}); //加超級源點
return spfa(n);
}
Johnson
- SPFA+Dijkstra實現全源最短路,編號從 $0$ 開始,\(O(VE\log E)\)
ll dis[N][N];
bool jn(){ //返回是否成功
if(negcycle())return 0;
repeat(x,0,n)
for(auto &i:a[x])
i.dis+=h[x]-h[i.to];
repeat(x,0,n)dij(x,dis[x]);
repeat(x,0,n)
repeat(p,0,n)
if(dis[x][p]!=inf)
dis[x][p]+=h[p]-h[x];
return 1;
}
最小環
- 有向圖最小環Dijkstra,\(O(VE\log E)\):對每個點 \(v\) 進行Dijkstra,到達 \(v\) 的邊更新答案,適用稀圖
- 有向圖最小環Floyd,\(O(V^3)\):Floyd完之后,任意兩點計算 \(dis_{u,v}+dis_{v,u}\),適用稠圖
- 無邊權無向圖最小環:以每個頂點為根生成bfs樹(不是dfs),橫叉邊更新答案,\(O(VE)\)
- 有邊權無向圖最小環:上面的bfs改成Dijkstra,\(O(VE \log E)\)
//無邊權無向圖最小環
int dis[N],fa[N],n,ans;
vector<int> a[N];
queue<int> q;
void bfs(int s){ //求經過s的最小環(不一定是簡單環)
fill(dis,dis+n,-1); dis[s]=0;
q.push(s); fa[s]=-1;
while(!q.empty()){
int x=q.front(); q.pop();
for(auto p:a[x])
if(p!=fa[x]){
if(dis[p]==-1){
dis[p]=dis[x]+1;
fa[p]=x;
q.push(p);
}
else ans=min(ans,dis[x]+dis[p]+1);
}
}
}
int mincycle(){
ans=inf;
repeat(i,0,n)bfs(i); //只要遍歷最小環可能經過的點即可
return ans;
}
最小生成樹×MST
Kruskal
- 對邊長排序,然后添邊,並查集判聯通,\(O(E\log E)\),排序是瓶頸
DSU d;
struct edge{int u,v,dis;}e[200010];
ll kru(){
ll ans=0,cnt=0;
sort(e,e+m);
repeat(i,0,m){
int x=d[e[i].u],y=d[e[i].v];
if(x==y)continue;
d.join(x,y);
ans+=e[i].dis;
cnt++;
if(cnt==n-1)break;
}
if(cnt!=n-1)return -1;
else return ans;
}
Boruvka
- 類似Prim算法,但是可以多路增廣(
名詞迷惑行為),\(O(E\log V)\)
DSU d;
struct edge{int u,v,dis;}e[200010];
ll bor(){
ll ans=0;
d.init(n);
e[m].dis=inf;
vector<int> b; //記錄每個聯通塊的增廣路(名詞迷惑行為)
bool f=1;
while(f){
b.assign(n,m);
repeat(i,0,m){
int x=d[e[i].u],y=d[e[i].v];
if(x==y)continue;
if(e[i].dis<e[b[x]].dis)
b[x]=i;
if(e[i].dis<e[b[y]].dis)
b[y]=i;
}
f=0;
for(auto i:b)
if(i!=m){
int x=d[e[i].u],y=d[e[i].v];
if(x==y)continue;
ans+=e[i].dis;
d.join(x,y);
f=1;
}
}
return ans;
}
最小樹形圖 | 朱劉算法
- 其實有更高級的Tarjan算法 \(O(E+V\log V)\),
但是學不會 - 編號從1開始,求的是葉向樹形圖,\(O(VE)\)
int n;
struct edge{int x,y,w;};
vector<edge> eset; //會在solve中被修改
ll solve(int rt){ //返回最小的邊權和,返回-1表示沒有樹形圖
static int fa[N],id[N],top[N],minw[N];
ll ans=0;
while(1){
int cnt=0;
repeat(i,1,n+1)
id[i]=top[i]=0,minw[i]=inf;
for(auto &i:eset) //記錄權最小的父親
if(i.x!=i.y && i.w<minw[i.y]){
fa[i.y]=i.x;
minw[i.y]=i.w;
}
minw[rt]=0;
repeat(i,1,n+1){ //標記所有環
if(minw[i]==inf)return -1;
ans+=minw[i];
for(int x=i;x!=rt && !id[x];x=fa[x])
if(top[x]==i){
id[x]=++cnt;
for(int y=fa[x];y!=x;y=fa[y])
id[y]=cnt;
break;
}
else top[x]=i;
}
if(cnt==0)return ans; //無環退出
repeat(i,1,n+1)
if(!id[i])
id[i]=++cnt;
for(auto &i:eset){ //縮點
i.w-=minw[i.y];
i.x=id[i.x],i.y=id[i.y];
}
n=cnt;
rt=id[rt];
}
}
樹論的一些概念
樹的直徑
- 直徑:即最長路徑
- 求直徑:以任意一點出發所能達到的最遠節點為一個端點,以這個端點出發所能達到的最遠節點為另一個端點(也可以樹上dp)
樹的重心
- 重心:以重心為根,其最大兒子子樹最小
- 求重心:dfs計算所有子樹大小,最后計算最大兒子子樹最小的節點
- 性質
- 以重心為根,所有子樹大小不超過整棵樹的一半
- 重心最多有兩個
- 重心到所有結點距離之和最小
- 兩棵樹通過一條邊相連,則新樹的重心在是原來兩棵樹重心的路徑上
- 一棵樹添加或刪除一個葉子,重心最多移動一條邊的距離
- 重心不一定在直徑上
樹鏈剖分
- 編號從 $0$ 開始,處理鏈 \(O(\log^2 n)\),處理子樹 \(O(\log n)\)
vector<int> e[N];
int dep[N],son[N],sz[N],top[N],fa[N];
int id[N],arcid[N],idcnt; //id[x]:結點x在樹剖序中的位置,arcid相反
void dfs1(int x){
sz[x]=1; son[x]=-1; dep[x]=dep[fa[x]]+1;
for(auto p:e[x]){
if(p==fa[x])continue;
fa[p]=x; dfs1(p);
sz[x]+=sz[p];
if(son[x]==-1 || sz[son[x]]<sz[p])
son[x]=p;
}
}
void dfs2(int x,int tv){
arcid[idcnt]=x; id[x]=idcnt++; top[x]=tv;
if(son[x]==-1)return;
dfs2(son[x],tv);
for(auto p:e[x]){
if(p==fa[x] || p==son[x])continue;
dfs2(p,p);
}
}
int lab[N]; //初始點權
seg tr[N*2],*pl; //if(l==r){a=lab[arcid[l]];return;}
void init(int s){
idcnt=0; fa[s]=s;
dfs1(s); dfs2(s,s);
seginit(0,idcnt-1); //線段樹的初始化
}
void upchain(int x,int y,int d){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])swap(x,y);
tr->update(id[top[x]],id[x],d);
x=fa[top[x]];
}
if(dep[x]>dep[y])swap(x,y);
tr->update(id[x],id[y],d);
}
ll qchain(int x,int y){
ll ans=0;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])swap(x,y);
ans+=tr->query(id[top[x]],id[x]);
x=fa[top[x]];
}
if(dep[x]>dep[y])swap(x,y);
ans+=tr->query(id[x],id[y]);
return ans;
}
void uptree(int x,int d){
tr->update(id[x],id[x]+sz[x]-1,d);
}
ll qtree(int x){
return tr->query(id[x],id[x]+sz[x]-1);
}
最近公共祖先
樹上倍增解法
- 編號從哪開始都可以,初始化 \(O(n\log n)\),查詢 \(O(\log n)\)
vector<int> e[N]; int dep[N],fa[N][22];
#define log(x) (31-__builtin_clz(x))
void dfs(int x){
repeat(i,1,log(dep[x])+1){
fa[x][i]=fa[fa[x][i-1]][i-1];
//dis[x][i]=U(dis[x][i-1],dis[fa[x][i-1]][i-1]);
}
for(auto p:e[x])
if(fa[x][0]!=p){
fa[p][0]=x,dep[p]=dep[x]+1,dfs(p);
//dis[p][0]=f(x,p);
}
}
int lca(int x,int y){
if(dep[x]<dep[y])swap(x,y);
while(dep[x]>dep[y])
x=fa[x][log(dep[x]-dep[y])];
if(x==y)return x;
repeat_back(i,0,log(dep[x])+1)
if(fa[x][i]!=fa[y][i])
x=fa[x][i],y=fa[y][i];
return fa[x][0];
}
void init(int s){fa[s][0]=s; dep[s]=0; dfs(s);}
/*
lf len2(int x,int y){ //y是x的祖先
lf ans=0;
while(dep[x]>dep[y]){
ans=U(ans,dis[x][log(dep[x]-dep[y])]);
x=fa[x][log(dep[x]-dep[y])];
}
return ans;
}
lf length(int x,int y){int l=lca(x,y); return U(len2(x,l),len2(y,l));} //無修查詢鏈上信息
*/
歐拉序列+st表解法
- 編號從 $0$ 開始,初始化 \(O(n\log n)\),查詢 \(O(1)\)
int n,m;
vector<int> a;
vector<int> e[500010];
bool vis[500010];
int pos[500010],dep[500010];
#define mininarr(a,x,y) (a[x]<a[y]?x:y)
struct RMQ{
#define logN 21
int f[N*2][logN],log[N*2];
RMQ(){
log[1]=0;
repeat(i,2,N*2)
log[i]=log[i/2]+1;
}
void build(){
int n=a.size();
repeat(i,0,n)
f[i][0]=a[i];
repeat(k,1,logN)
repeat(i,0,n-(1<<k)+1)
f[i][k]=mininarr(dep,f[i][k-1],f[i+(1<<(k-1))][k-1]);
}
int query(int l,int r){
if(l>r)swap(l,r);//!!
int s=log[r-l+1];
return mininarr(dep,f[l][s],f[r-(1<<s)+1][s]);
}
}rmq;
void dfs(int x,int d){
if(vis[x])return;
vis[x]=1;
dep[x]=d;
a.push_back(x);
pos[x]=a.size()-1; //記錄位置
repeat(i,0,e[x].size()){
int p=e[x][i];
if(vis[p])continue;
dfs(p,d+1);
a.push_back(x);
}
}
int lca(int x,int y){
return rmq.query(pos[x],pos[y]);
}
//初始化:dfs(s,1); rmq.build();
樹鏈剖分解法
- 編號從哪開始都可以,初始化 \(O(n)\),查詢 \(O(\log n)\)
vector<int> e[N];
int dep[N],son[N],sz[N],top[N],fa[N]; //son重兒子,top鏈頂
void dfs1(int x){ //標注dep,sz,son,fa
sz[x]=1;
son[x]=-1;
dep[x]=dep[fa[x]]+1;
for(auto p:e[x]){
if(p==fa[x])continue;
fa[p]=x; dfs1(p);
sz[x]+=sz[p];
if(son[x]==-1 || sz[son[x]]<sz[p])
son[x]=p;
}
}
void dfs2(int x,int tv){ //標注top
top[x]=tv;
if(son[x]==-1)return;
dfs2(son[x],tv);
for(auto p:e[x]){
if(p==fa[x] || p==son[x])continue;
dfs2(p,p);
}
}
void init(int s){ //s是根
fa[s]=s;
dfs1(s);
dfs2(s,s);
}
int lca(int x,int y){
while(top[x]!=top[y])
if(dep[top[x]]>=dep[top[y]])x=fa[top[x]];
else y=fa[top[y]];
return dep[x]<dep[y]?x:y;
}
Tarjan解法
- 離線算法,基於並查集
- qry 和 ans 編號從 $0$ 開始,\(O(n+m)\),大常數(不看好)
vector<int> e[N]; vector<pii> qry,q[N]; //qry輸入
DSU d; bool vis[N]; int ans[N]; //ans輸出
void dfs(int x){
vis[x]=1;
for(auto i:q[x])if(vis[i.fi])ans[i.se]=d[i.fi];
for(auto p:e[x])if(!vis[p])dfs(p),d[p]=x;
}
void solve(int n,int s){
repeat(i,0,qry.size()){
q[qry[i].fi].push_back({qry[i].se,i});
q[qry[i].se].push_back({qry[i].fi,i});
}
d.init(n); dfs(s);
}
一些關於lca的問題
int length(int x,int y){ //路徑長度
return dep[x]+dep[y]-2*dep[lca(x,y)];
}
聯通性相關
強聯通分量scc+縮點 | Tarjan
- 編號從0開始,\(O(V+E)\)
vector<int> a[N];
stack<int> stk;
bool vis[N],instk[N];
int dfn[N],low[N],co[N],w[N]; //co:染色結果,w:點權
vector<int> sz; //sz:第i個顏色的點數
int n,m,dcnt;
void dfs(int x){ //Tarjan求強聯通分量
vis[x]=instk[x]=1; stk.push(x);
dfn[x]=low[x]=++dcnt;
for(auto p:a[x]){
if(!vis[p])dfs(p);
if(instk[p])low[x]=min(low[x],low[p]);
}
if(low[x]==dfn[x]){
int t; sz.push_back(0); //記錄
do{
t=stk.top();
stk.pop();
instk[t]=0;
sz.back()+=w[t]; //記錄
co[t]=sz.size()-1; //染色
}while(t!=x);
}
}
void getscc(){
fill(vis,vis+n,0);
sz.clear();
repeat(i,0,n)if(!vis[i])dfs(i);
}
void shrink(){ //縮點,在a里重構
static set<pii> eset;
eset.clear();
getscc();
repeat(i,0,n)
for(auto p:a[i])
if(co[i]!=co[p])
eset.insert({co[i],co[p]});
n=sz.size();
repeat(i,0,n){
a[i].clear();
w[i]=sz[i];
}
for(auto i:eset){
a[i.fi].push_back(i.se);
//a[i.se].push_back(i.fi);
}
}
- 例題:給一個有向圖,連最少的邊使其變為scc。解:scc縮點后輸出 \(\max(\sum\limits_i[indeg[i]=0],\sum\limits_i[outdeg[i]=0])\),特判只有一個scc的情況
邊雙連通分量 | Tarjan
- 編號從0開始,\(O(V+E)\)
void dfs(int x,int fa){ //Tarjan求邊雙聯通分量
vis[x]=instk[x]=1; stk.push(x);
dfn[x]=low[x]=++dcnt;
for(auto p:a[x])
if(p!=fa){
if(!vis[p])dfs(p,x);
if(instk[p])low[x]=min(low[x],low[p]);
}
else fa=-1; //處理重邊
if(low[x]==dfn[x]){
int t; sz.push_back(0); //記錄
do{
t=stk.top();
stk.pop();
instk[t]=0;
sz.back()+=w[t]; //記錄
co[t]=sz.size()-1; //染色
}while(t!=x);
}
}
void getscc(){
fill(vis,vis+n,0);
sz.clear();
repeat(i,0,n)if(!vis[i])dfs(i,-1);
}
//全局變量,shrink()同scc
割點×割頂
- Tarjan
bool vis[N],cut[N]; //cut即結果,cut[i]表示i是否為割點
int dfn[N],low[N];
int dcnt; //時間戳
void dfs(int x,bool isroot=1){
if(vis[x])return; vis[x]=1;
dfn[x]=low[x]=++dcnt;
int ch=0; cut[x]=0;
for(auto p:a[x]){
if(!vis[p]){
dfs(p,0);
low[x]=min(low[x],low[p]);
if(!isroot && low[p]>=dfn[x])
cut[x]=1;
ch++;
}
low[x]=min(low[x],dfn[p]);
}
if(isroot && ch>=2) //根節點判斷方法
cut[x]=1;
}
2-sat問題
可行解
- 有 $2n$ 個頂點,其中頂點 $2i$ 和頂點 $2i+1$ 中能且僅能選一個,邊 \((u,v)\) 表示選了 \(u\) 就必須選 \(v\),求一個可行解
- 暴力版,可以跑出字典序最小的解,編號從 $0$ 開始,\(O(VE)\),(
但是難以跑到上界)
struct twosat{ //暴力版
int n;
vector<int> g[N*2];
bool mark[N*2]; //mark即結果,表示是否選擇了這個點
int s[N],c;
bool dfs(int x){
if(mark[x^1])return 0;
if(mark[x])return 1;
mark[s[c++]=x]=1;
for(auto p:g[x])
if(!dfs(p))
return 0;
return 1;
}
void init(int _n){
n=_n;
for(int i=0;i<n*2;i++){
g[i].clear();
mark[i]=0;
}
}
void add(int x,int y){ //這個函數隨題意變化
g[x].push_back(y^1); //選了x就必須選y^1
g[y].push_back(x^1); //選了y就必須選x^1
}
bool solve(){ //返回是否存在解
for(int i=0;i<n*2;i+=2)
if(!mark[i] && !mark[i^1]){
c=0;
if(!dfs(i)){
while(c>0)mark[s[--c]]=0;
if(!dfs(i^1))return 0;
}
}
return 1;
}
}ts;
- SCC縮點版,\(O(V+E)\),空缺
- 2-SAT計數
- 空缺(太恐怖了)
圖上的NP問題
最大團+極大團計數
- 求最大團頂點數(和最大團),
g[][]
編號從 $0$ 開始,\(O(\exp)\)
int g[N][N],f[N][N],v[N],Max[N],n,ans; //g[][]是鄰接矩陣,n是頂點數
//vector<int> rec,maxrec; //maxrec是最大團
bool dfs(int x,int cur){
if(cur==0)
return x>ans;
repeat(i,0,cur){
int u=f[x][i],k=0;
if(Max[u]+x<=ans)return 0;
repeat(j,i+1,cur)
if(g[u][f[x][j]])
f[x+1][k++]=f[x][j];
//rec.push_back(u);
if(dfs(x+1,k))return 1;
//rec.pop_back();
}
return 0;
}
void solve(){
ans=0; //maxrec.clear();
repeat_back(i,0,n){
int k=0;
repeat(j,i+1,n)
if(g[i][j])
f[1][k++]=j;
//rec.clear(); rec.push_back(i);
if(dfs(1,k)){
ans++;
//maxrec=rec;
}
Max[i]=ans;
}
}
- 求極大團個數(和所有極大團),
g[][]
的編號從 $1$ 開始!\(O(\exp)\)
int g[N][N],n;
//vector<int> rec; //存當前極大團
int ans,some[N][N],none[N][N]; //some是未搜索的點,none是廢除的點
void dfs(int d,int sn,int nn){
if(sn==0 && nn==0)
ans++; //此時rec是其中一個極大圖
//if(ans>1000)return; //題目要求_(:зゝ∠)_
int u=some[d][0];
for(int i=0;i<sn;++i){
int v=some[d][i];
if(g[u][v])continue;
int tsn=0,tnn=0;
for(int j=0;j<sn;++j)
if(g[v][some[d][j]])
some[d+1][tsn++]=some[d][j];
for(int j=0;j<nn;++j)
if(g[v][none[d][j]])
none[d+1][tnn++]=none[d][j];
//rec.push_back(v);
dfs(d+1,tsn,tnn);
//rec.pop_back();
some[d][i]=0;
none[d][nn++]=v;
}
}
void solve(){ //運行后ans即極大團數
ans=0;
for(int i=0;i<n;++i)
some[0][i]=i+1;
dfs(0,n,0);
}
最小染色數
- \(O(\exp)\),
n=17
可用
int n,m;
int g[N]; //二進制鄰接矩陣
bool ind[1<<N]; //是否為(極大)獨立集
int dis[1<<N];
vector<int> a; //存獨立集
#define np (1<<n)
int bfs(){ //重復覆蓋簡略版
fill(dis,dis+np,inf); dis[0]=0;
auto q=queue<int>(); q.push(0);
while(!q.empty()){
int x=q.front(); q.pop();
for(auto i:a){
int p=x|i;
if(p==np-1)return dis[x]+1;
if(dis[p]>dis[x]+1){
dis[p]=dis[x]+1;
q.push(p);
}
}
}
return 0;
}
int solve(){ //返回最小染色數
mst(g,0);
for(auto i:eset){
int x=i.fi,y=i.se;
g[x]|=1<<y;
g[y]|=1<<x;
}
//求所有獨立集
ind[0]=1;
repeat(i,1,np){
int w=63-__builtin_clzll(ll(i)); //最高位
if((g[w]&i)==0 && ind[i^(1<<w)])
ind[i]=1;
}
//刪除所有不是極大獨立集的獨立集
repeat(i,1,np)
if(ind[i]){
for(int j=1;j<np;j<<=1)
if((i&j)==0 && ind[i|j]){
ind[i]=0;
break;
}
if(ind[i])
a.push_back(i); //記錄極大獨立集
}
return bfs();
}
弦圖+區間圖
- 弦是連接環上不相鄰點的邊;弦圖是所有長度大於3的環都有弦的無向圖(類似三角剖分)
- 單純點:所有與v相連的點構成一個團,則v是一個單純點
- 完美消除序列:即點集的一個排列 \([v_1,v_2,...,v_n]\) 滿足任意 \(v_i\) 在 \([v_{i+1},...,v_n]\) 的導出子圖中是一個單純點
- 定理:無向圖是弦圖 \(\Leftrightarrow\) 無向圖存在完美消除序列
- 定理:最大團頂點數 \(\le\) 最小染色數(弦圖取等號)
- 定理:最大獨立集頂點數 \(\le\) 最小團覆蓋(弦圖取等號)
- 最大勢算法MCS求完美消除序列:每次求出與 \([v_{i+1},...,v_n]\) 相鄰點數最大的點作為 \(v_i\)
e[][]
點編號從 $1$ 開始!rec
下標從 $1$ 開始!桶優化,\(O(V+E)\)
vector<int> e[N];
int n,rec[N]; //rec[1..n]是結果
int h[N],nxt[N],pre[N],vis[N],lab[N];
void del(int x){
int w=lab[x];
if(h[w]==x)h[w]=nxt[x];
pre[nxt[x]]=pre[x];
nxt[pre[x]]=nxt[x];
}
void mcs(){
fill(h,h+n+1,0);
fill(vis,vis+n+1,0);
fill(lab,lab+n+1,0);
iota(nxt,nxt+n+1,1);
iota(pre,pre+n+1,-1);
nxt[n]=0;
h[0]=1;
int w=0;
repeat_back(i,1,n+1){
int x=h[w];
rec[i]=x;
del(x);
vis[x]=1;
for(auto p:e[x])
if(!vis[p]){
del(p);
lab[p]++;
nxt[p]=h[lab[p]];
pre[h[lab[p]]]=p;
h[lab[p]]=p;
pre[p]=0;
}
w++;
while(h[w]==0)w--;
}
}
- 判斷弦圖(判斷是否為完美消除序列):對所有 \(v_i\),\([v_{i+1},...,v_n]\) 中與 \(v_i\) 相連的最靠前一個點 \(v_j\) 是否與與 \(v_i\) 連接的其他點相連
- 編號規則同上,大佬:\(O(V+E)\),我:\(O((V+E)\log V)\)
bool judge(){ //返回是否是完美消除序列(先要跑一遍MCS)
static int s[N],rnk[N];
repeat(i,1,n+1){
rnk[rec[i]]=i;
sort(e[i].begin(),e[i].end()); //方便二分查找,內存足夠直接unmap
}
repeat(i,1,n+1){
int top=0,x=rec[i];
for(auto p:e[x])
if(rnk[x]<rnk[p]){
s[++top]=p;
if(rnk[s[top]]<rnk[s[1]])
swap(s[1],s[top]);
}
repeat(j,2,top+1)
if(!binary_search(e[s[1]].begin(),e[s[1]].end(),s[j]))
return 0;
}
return 1;
}
- 其他弦圖算法
int color(){ //返回最大團點數/最小染色數
return *max_element(lab+1,lab+n+1)+1;
/* //以下求最大團
static int rnk[N];
repeat(i,1,n+1)rnk[rec[i]]=i;
int x=max_element(lab+1,lab+n+1)-lab;
rec2.push_back(x);
for(auto p:e[x])
if(rnk[x]<rnk[p])
rec2.push_back(x);
*/
}
int maxindset(){ //返回最大獨立集點數/最小團覆蓋數
int ans=0;
fill(vis,vis+n+1,0);
repeat(i,1,n+1){
int x=rec[i];
if(!vis[x]){
ans++; //rec2.push_back(x); //記錄最大獨立集
for(auto p:e[x])
vis[p]=1;
}
}
return ans;
}
int cliquecnt(){ //返回極大團數
static int s[N],fst[N],rnk[N],cnt[N];
int ans=0;
repeat(i,1,n+1)rnk[rec[i]]=i;
repeat(i,1,n+1){
int top=0,x=rec[i];
for(auto p:e[x])
if(rnk[x]<rnk[p]){
s[++top]=p;
if(rnk[s[top]]<rnk[s[1]])
swap(s[1],s[top]);
}
fst[x]=s[1]; cnt[x]=top;
}
fill(vis,vis+n+1,0);
repeat(i,1,n+1){
int x=rec[i];
if(!vis[x])ans++;
if(cnt[x]>0 && cnt[x]>=cnt[fst[x]]+1)
vis[fst[x]]=1;
}
return ans;
}
- 區間圖:給出的每個區間都看成點,有公共部分的兩個區間之間連一條邊
- 區間圖是弦圖(反過來不一定),可以應用弦圖的所有算法
- 區間圖的判定:所有弦圖可以寫成一個極大團樹(所有極大團看成一個頂點,極大團之間有公共頂點就連一條邊),區間圖的極大團樹是一個鏈
仙人掌 | 圓方樹
- 仙人掌:每條邊至多屬於一個簡單環的無向聯通圖
- 圓方樹:原來的點稱為圓點,每個環新建一個方點,環上的圓點都與方點連接
- 子仙人掌:以 \(r\) 為根,點 \(p\) 的子仙人掌是刪掉 \(p\) 到 \(r\) 的所有簡單路徑后 \(p\) 所在的聯通塊。這個子仙人掌就是圓方樹中以 \(r\) 為根時,\(p\) 子樹中的所有圓點
- 仙人掌的判定(樹上差分)編號從哪開始都可以,\(O(n+m)\)
vector<int> a[N]; //vector<int> rec; //rec存每個環的大小
bool vis[N]; int fa[N],lab[N],dep[N]; bool ans;
void dfs(int x){
vis[x]=1;
for(auto p:a[x])if(p!=fa[x]){
if(!vis[p]){
fa[p]=x; dep[p]=dep[x]+1;
dfs(p); lab[x]+=lab[p];
}
else if(dep[p]<dep[x]){
lab[x]++; lab[p]--;
//rec.push_back(dep[x]-dep[p]+1);
}
}
if(lab[x]>=2)ans=0;
}
bool iscactus(int s){
fill(vis,vis+n+1,0);
ans=1; fa[s]=-1; dfs(s); return ans;
}
二分圖
二分圖的一些概念
- 最小點覆蓋(最小的點集,使所有邊都能被覆蓋) = 最大匹配
- 最大獨立集 = 頂點數 - 最大匹配
- 最小路徑覆蓋 = (開點前)頂點數 - 最大匹配,右頂點未被匹配的都看作起點
- 最小帶權點覆蓋 = 點權之和 - 最大帶權獨立集(左式用最小割求)
- 霍爾定理:最大匹配 = 左頂點數 \(\Leftrightarrow\) 所有左頂點子集 \(S\) 都有 \(|S|\le|\omega(S)|\) ,\(\omega(S)\) 是 \(S\) 的領域
- 運用:若在最大匹配中有 \(t\) 個左頂點失配,因此最大匹配 = 左頂點數 - \(t\)
- 對任意左頂點子集 \(S\) 都有 \(|S|\le|\omega(S)|+t\),\(t\ge|S|-|\omega(S)|\) ,求右式最大值即可求最大匹配
最大匹配 | 匈牙利hungarian or 網絡流
- 匈牙利,編號從 $0$ 開始,\(O(VE)\)
int n,m; //n個左頂點,m個右頂點
vector<int> a[N]; //左頂點x與右頂點a[x][0..sz]有連接
int dcnt,mch[N],dfn[N]; //mch[p]表示與右頂點p連接的左頂點,dfn[p]==dcnt意味着右頂點p已訪問
bool dfs(int x){
repeat(i,0,a[x].size()){
int p=a[x][i];
if(dfn[p]!=dcnt){
dfn[p]=dcnt;
if(mch[p]==-1 || dfs(mch[p])){
mch[p]=x;
return 1;
}
}
}
return 0;
}
int hun(int n,int m){ //返回最大匹配數
int ans=0;
repeat(i,0,m)mch[i]=dfn[i]=-1; //初始化
repeat(i,0,n){
dcnt=i;
if(dfs(i))ans++;
}
return ans;
}
- 網絡流建圖,編號從 $0$ 開始,\(O(V^{\tfrac 1 2}E)\)
int work(int n1,int n2,vector<pii> &eset){
int n=n1+n2+2;
int s=0,t=n1+n2+1;
flow.init(n);
repeat(i,1,n1+1)add(s,i,1);
repeat(i,n1+1,n1+n2+1)add(i,t,1);
for(const auto &i:eset){
int x=i.fi,y=i.se;
add(x+1,n1+y+1,1);
}
return flow.solve(s,t);
}
最大權匹配 | KM
- 求滿二分圖的最大權匹配
- 如果沒有邊就建零邊,而且要求n<=m
- 編號從 $0$ 開始,\(O(n^3)\)
int e[N][N],n,m; //鄰接矩陣,左頂點數,右頂點數
int lx[N],ly[N]; //頂標
int mch[N]; //右頂點i連接的左頂點編號
bool fx[N],fy[N]; //是否在增廣路上
bool dfs(int i){
fx[i]=1;
repeat(j,0,n)
if(lx[i]+ly[j]==e[i][j] && !fy[j]){
fy[j]=1;
if(mch[j]==-1 || dfs(mch[j])){
mch[j]=i;
return 1;
}
}
return 0;
}
void update(){
int fl=inf;
repeat(i,0,n)if(fx[i])
repeat(j,0,m)if(!fy[j])
fl=min(fl,lx[i]+ly[j]-e[i][j]);
repeat(i,0,n)if(fx[i])lx[i]-=fl;
repeat(j,0,m)if(fy[j])ly[j]+=fl;
}
int solve(){ //返回匹配數
repeat(i,0,n){
mch[i]=-1;
lx[i]=ly[i]=0;
repeat(j,0,m)
lx[i]=max(lx[i],e[i][j]);
}
repeat(i,0,n)
while(1){
repeat(j,0,m)
fx[j]=fy[j]=0;
if(dfs(i))break;
else update();
}
int ans=0;
repeat(i,0,m)
if(mch[i]!=-1)
ans+=e[mch[i]][i];
return ans;
}
穩定婚姻 | 延遲認可
- 穩定意味着不存在一對不是情侶的男女,都認為當前伴侶不如對方
- 編號從 $0$ 開始,\(O(n^2)\)
struct node{
int s[N]; //s的值給定
//對男生來說是女生編號排序
//對女生來說是男生的分數
int now; //選擇的伴侶編號
}a[N],b[N]; //男生,女生
int tr[N]; //男生嘗試表白了幾次
queue<int> q; //單身狗(男)排隊
bool match(int x,int y){ //配對,返回是否成功
int x0=b[y].now;
if(x0!=-1){
if(b[y].s[x]<b[y].s[x0])
return 0; //分數不夠,競爭失敗
q.push(x0);
}
a[x].now=y;
b[y].now=x;
return 1;
}
void stable_marriage(){ //運行后a[].now,b[].now即結果
q=queue<int>();
repeat(i,0,n){
b[i].now=-1;
q.push(i);
tr[i]=0;
}
while(!q.empty()){
int x=q.front(); q.pop();
int y=a[x].s[tr[x]++]; //下一個最中意女生
if(!match(x,y))
q.push(x); //下次努力
}
}
一般圖最大匹配 | 帶花樹
- 對於一個無向圖,找最多的邊使得這些邊兩兩無公共端點
- 編號從 $1$ 開始,\(O(n^3)\)
int n; DSU d;
deque<int> q; vector<int> e[N];
int mch[N],vis[N],dfn[N],fa[N],dcnt=0;
int lca(int x,int y){
dcnt++;
while(1){
if(x==0)swap(x,y); x=d[x];
if(dfn[x]==dcnt)return x;
else dfn[x]=dcnt,x=fa[mch[x]];
}
}
void shrink(int x,int y,int p){
while(d[x]!=p){
fa[x]=y; y=mch[x];
if(vis[y]==2)vis[y]=1,q.push_back(y);
if(d[x]==x)d[x]=p;
if(d[y]==y)d[y]=p;
x=fa[y];
}
}
bool match(int s){
d.init(n); fill(fa,fa+n+1,0);
fill(vis,vis+n+1,0); vis[s]=1;
q.assign(1,s);
while(!q.empty()){
int x=q.front(); q.pop_front();
for(auto p:e[x]){
if(d[x]==d[p] || vis[p]==2)continue;
if(!vis[p]){
vis[p]=2; fa[p]=x;
if(!mch[p]){
for(int now=p,last,tmp;now;now=last){
last=mch[tmp=fa[now]];
mch[now]=tmp,mch[tmp]=now;
}
return 1;
}
vis[mch[p]]=1; q.push_back(mch[p]);
}
else if(vis[p]==1){
int l=lca(x,p);
shrink(x,p,l);
shrink(p,x,l);
}
}
}
return 0;
}
int solve(){ //返回匹配數,mch[]是匹配結果(即匹配x和mch[x]),==0表示不匹配
int ans=0; fill(mch,mch+n+1,0);
repeat(i,1,n+1)ans+=(!mch[i] && match(i));
return ans;
}
- 例題:給定一個無向圖和 \(d_i\)($1\le d_i\le 2$),求是否能刪去一些邊后滿足點 \(i\) 的度剛好是 \(d_i\)
::n=n*2+m*2; //::n是帶花樹板子里的n
repeat(i,1,n+1)cnt+=deg[i]=read();
repeat(i,1,m+1){
int x=read(),y=read();
if(deg[x]==2 && deg[y]==2){ //(x,e)(x',e)(y,e')(y',e')(e,e')
add(x,n*2+i),add(x+n,n*2+i),add(y,n*2+m+i),add(y+n,n*2+m+i),add(n*2+i,n*2+m+i);
cnt+=2;
}
else{ //(x,y),度為2再添一條邊
add(x,y); if(deg[x]==2)add(x+n,y); if(deg[y]==2)add(x,y+n);
}
}
puts(solve()*2==cnt?"Yes":"No");
網絡流
網絡流的一些概念
- \(c(u,v)\) 為 \(u\) 到 \(v\) 的容量,\(f(u,v)\) 為 \(u\) 到 \(v\) 的流量,\(f(u,v)<c(u,v)\)
- \(c[X,Y]\) 為 \(X\) 到 \(Y\) 的容量和,不包括 \(Y\) 到 \(X\) 的容量;\(f(X,Y)\) 為 \(X\) 到 \(Y\) 的流量和,要減去 \(Y\) 到 \(X\) 的流量
- 費用流(最小費用最大流):保證最大流后的最小費用
- 割:割 \([S,T]\) 是點集的一個分割且 \(S\) 包含源點,\(T\) 包含匯點,稱 \(f(S,T)\) 為割的凈流,\(c[S,T]\) 為割的容量
- 最大流最小割定理:最大流即最小割容量
- 求最小割:在最大流殘量網絡中,令源點可達的點集為 \(S\),其余的為 \(T\) 即可(但是滿流邊不一定都在 \(S,T\) 之間)
- 閉合子圖:子圖內所有點的兒子都在子圖內。點權之和最大的閉合子圖為最大閉合子圖
- 求最大閉合子圖:點權為正則s向該點連邊,邊權為點權,為負則向t連邊,邊權為點權絕對值,原圖所有邊的權設為inf,跑最小割。如果連s的邊被割則不選這個點,若連t的邊被割則選這個點
最大流
- 以下頂點編號均從 $0$ 開始
Dinic
- 多路增廣,\(O(V^2E)\)
struct FLOW{
struct edge{int to,w,nxt;};
vector<edge> a; int head[N],cur[N];
int n,s,t;
queue<int> q; bool inque[N];
int dep[N];
void ae(int x,int y,int w){ //add edge
a.push_back({y,w,head[x]});
head[x]=a.size()-1;
}
bool bfs(){ //get dep[]
fill(dep,dep+n,inf); dep[s]=0;
copy(head,head+n,cur);
q=queue<int>(); q.push(s);
while(!q.empty()){
int x=q.front(); q.pop(); inque[x]=0;
for(int i=head[x];i!=-1;i=a[i].nxt){
int p=a[i].to;
if(dep[p]>dep[x]+1 && a[i].w){
dep[p]=dep[x]+1;
if(inque[p]==0){
inque[p]=1;
q.push(p);
}
}
}
}
return dep[t]!=inf;
}
int dfs(int x,int flow){ //extend
int now,ans=0;
if(x==t)return flow;
for(int &i=cur[x];i!=-1;i=a[i].nxt){
int p=a[i].to;
if(a[i].w && dep[p]==dep[x]+1)
if((now=dfs(p,min(flow,a[i].w)))){
a[i].w-=now;
a[i^1].w+=now;
ans+=now,flow-=now;
if(flow==0)break;
}
}
return ans;
}
void init(int _n){
n=_n+1; a.clear();
fill(head,head+n,-1);
fill(inque,inque+n,0);
}
int solve(int _s,int _t){ //return max flow
s=_s,t=_t;
int ans=0;
while(bfs())ans+=dfs(s,inf);
return ans;
}
}flow;
void add(int x,int y,int w){flow.ae(x,y,w),flow.ae(y,x,0);}
//先flow.init(n),再add添邊,最后flow.solve(s,t)
ISAP
- 僅一次bfs與多路增廣,\(O(V^2E)\),有鍋!!
struct FLOW{
struct edge{int to,w,nxt;};
vector<edge> a; int head[N];
int cur[N];
int n,s,t;
queue<int> q;
int dep[N],gap[N];
void ae(int x,int y,int w){
a.push_back({y,w,head[x]});
head[x]=a.size()-1;
}
bool bfs(){
fill(dep,dep+n,-1); dep[t]=0;
fill(gap,gap+n,0); gap[0]=1;
q.push(t);
while(!q.empty()){
int x=q.front(); q.pop();
for(int i=head[x];i!=-1;i=a[i].nxt){
int p=a[i].to;
if(dep[p]!=-1)continue;
dep[p]=dep[x]+1;
q.push(p);
gap[dep[p]]++;
}
}
return dep[s]!=-1;
}
int dfs(int x,int fl){
int now,ans=0;
if(x==t)return fl;
for(int i=cur[x];i!=-1;i=a[i].nxt){
cur[x]=i;
int p=a[i].to;
if(a[i].w && dep[p]+1==dep[x])
if((now=dfs(p,min(fl,a[i].w)))){
a[i].w-=now;
a[i^1].w+=now;
ans+=now,fl-=now;
if(fl==0)return ans;
}
}
gap[dep[x]]--;
if(gap[dep[x]]==0)dep[s]=n;
dep[x]++;
gap[dep[x]]++;
return ans;
}
void init(int _n){
n=_n+1;
a.clear();
fill(head,head+n,-1);
}
int solve(int _s,int _t){ //返回最大流
s=_s,t=_t;
int ans=0;
if(bfs())
while(dep[s]<n){
copy(head,head+n,cur);
ans+=dfs(s,inf);
}
return ans;
}
}flow;
void add(int x,int y,int w){flow.ae(x,y,w),flow.ae(y,x,0);}
//先flow.init(n),再add添邊,最后flow.solve(s,t)
最小費用最大流 | MCMF
費用流一般指最小費用最大流(最大費用最大流把費用取反即可)
MCMF,單路增廣,\(O(VE^2)\)(其實還有類Dinic算法,
有空再補)
struct FLOW{ //MCMF費用流
struct edge{int to,w,cost,nxt;}; //指向,限流,費用,下一條邊
vector<edge> a; int head[N]; //前向星
int n,s,t,totcost; //點數,源點,匯點,總費用
deque<int> q;
bool inque[N]; //在隊里的不需要入隊
int dis[N]; //費用
struct{int to,e;}pre[N]; //路徑的前一個點,這條邊的位置
void ae(int x,int y,int w,int cost){
a.push_back((edge){y,w,cost,head[x]});
head[x]=a.size()-1;
}
bool spfa(){ //已死的算法
fill(dis,dis+n,inf); dis[s]=0;
q.assign(1,s);
while(!q.empty()){
int x=q.front(); q.pop_front();
inque[x]=0;
for(int i=head[x];i!=-1;i=a[i].nxt){
int p=a[i].to;
if(dis[p]>dis[x]+a[i].cost && a[i].w){
dis[p]=dis[x]+a[i].cost;
pre[p]={x,i};
if(inque[p]==0){
inque[p]=1;
if(!q.empty()
&& dis[q.front()]<=dis[p])
q.push_back(p);
else q.push_front(p);
//松弛,或者直接q.push_back(p);
}
}
}
}
return dis[t]!=inf;
}
void init(int _n){ //初始化
n=_n+1;
a.clear();
fill(head,head+n,-1);
fill(inque,inque+n,0);
}
int solve(int _s,int _t){ //返回最大流,費用存totcost里
s=_s,t=_t;
int ans=0;
totcost=0;
while(spfa()){
int fl=inf;
for(int i=t;i!=s;i=pre[i].to)
fl=min(fl,a[pre[i].e].w);
for(int i=t;i!=s;i=pre[i].to){
a[pre[i].e].w-=fl;
a[pre[i].e^1].w+=fl;
}
totcost+=dis[t]*fl;
ans+=fl;
}
return ans;
}
}flow;
void add(int x,int y,int w,int cost){
flow.ae(x,y,w,cost),flow.ae(y,x,0,-cost);
}
//先flow.init(n),再add添邊,最后flow.solve(s,t)
圖論雜項
矩陣樹定理
無向圖矩陣樹定理
- 生成樹計數
void matrix::addedge(int x,int y){
a[x][y]--,a[y][x]--;
a[x][x]++,a[y][y]++;
}
lf matrix::treecount(){
//for(auto i:eset)addedge(i.fi,i.se); //加邊
n--,m=n; //a[n-1][n-1]的余子式(選任一節點均可)
return get_det();
}
有向圖矩陣樹定理
- 根向樹形圖計數,每條邊指向父親
- (葉向樹形圖,即每條邊指向兒子,只要修改一個地方)
- 如果要求所有根的樹形圖之和,就求逆的主對角線之和乘以行列式(\(A^*=|A|A^{-1}\))
void matrix::addedge(int x,int y){
a[x][y]--;
a[x][x]++; //葉向樹形圖改成a[y][y]++;
}
ll matrix::treecount(){
//for(auto i:eset)addedge(i.fi,i.se); //加邊
repeat(i,s,n) //s是根節點
repeat(j,0,n)
a[i][j]=a[i+1][j];
repeat(i,0,n)
repeat(j,s,n)
a[i][j]=a[i][j+1];
n--,m=n; //a[s][s]的余子式
return get_det();
}
BSET定理
- 有向歐拉圖的歐拉回路總數等於任意根的根向樹形圖個數乘以 \(\Pi(deg(v)-1)!\)(←階乘)(\(deg(v)\) 是 \(v\) 的入度或出度,
反正入度等於出度)
Enumerative properties of Ferrers graphs
- 二分圖,左頂點連編號為 $1,2,...,a_i$ 的右頂點,則該圖的生成樹個數為 \(\dfrac{\prod\limits_{i∈A}deg_i}{\max\limits_{i∈A}deg_i}\cdot\dfrac{\prod\limits_{i∈B}deg_i}{\max\limits_{i∈B}deg_i}\) 左頂點度之積(去掉度最大的)乘以右頂點度之積(去掉度最大的)
Prufer序列
- \(n\) 個點的無根樹與長度 \(n-2\) 值域 \([1,n]\) 的序列有雙射關系,Prufer序列就是其中一種
- 無根樹轉Prufer:設無根樹點數為 \(n\),每次刪除編號最小的葉子並記錄它所連接的點的編號,進行 \(n-2\) 次操作
- Prufer轉無根樹:計算每個點的度為在序列中出現的次數加 $1$,每次找度為 $1$ 的編號最小的點與序列中第一個點連接,並將后者的度減 $1$
- Cayley定理:完全圖 \(K_n\) 有 \(n^{n-2}\) 棵生成樹
- 擴展:\(k\) 個聯通塊,第 \(i\) 個聯通塊有 $s_i$個點,則添加 \(k-1\) 條邊使整個圖聯通的方案數有 \(n^{k-2}\Pi_{i=1}^k s_i\) 個
LGV引理
- DAG上固定 $2n$ 個點 \([A_1,\cdots,A_n,B_1,\cdots,B_n]\),若有 \(n\) 條路徑 \([A_1→B_1,\cdots,A_n→B_n]\) 兩兩不相交,則方案數為
- \(M=\left|\begin{array}{c}e(A_1,B_1)&\cdots &e(A_1,B_n)\\\vdots&\ddots&\vdots\\e(A_n,B_1)&\cdots&e(A_n,B_n)\end{array}\right|\)
- 其中 \(e(u,v)\) 表示 \(u→v\) 的路徑計數
others of 圖論雜項
Havel-Hakimi定理
- 給定一個度序列,反向構造出這個圖
- 解:貪心,每次讓剩余度最大的頂點 \(k\) 連接其余頂點中剩余度最大的 \(deg_k\) 個頂點
- (我認為二路歸並比較快,可是找到的代碼都用了
sort()
)
無向圖三元環計數
- 無向圖定向,\(pii(deg_i,i)>pii(deg_j,j)\Leftrightarrow\) 建立有向邊 \((i,j)\)。然后暴力枚舉 \(u\),將 \(u\) 的所有兒子 \(\omega(u)\) 標記為 \(dcnt\),暴力枚舉 \(v∈\omega(u)\),若 \(v\) 的兒子被標記為 \(dcnt\) 則 \(ans++\),\(O(E\log E)\)
字符串
- (
我字符串是最菜的) - 尋找模式串p在文本串t中的所有出現
哈希×Hash
字符串哈希
- 如果不需要區間信息,可以調用
hash<string>()(s)
獲得ull范圍的hash值 - 碰撞概率:單哈希 $10^6$ 次比較大約有 \(\dfrac 1 {1000}\) 概率碰撞
- 支持查詢子串hash值,初始化 \(O(n)\),子串查詢 \(O(1)\)
const int hashxor=rnd()%1000000000; //如果不是cf可以不用hashxor
struct Hash{
vector<ll> a[2],p[2];
const ll b=257,m[2]={1000000007,998244353};
Hash(){repeat(i,0,2)a[i]={0},p[i]={1};}
void push(const string &s){
repeat(i,0,2)
for(auto c:s){
a[i]+=(a[i].back()*b+(c^hashxor))%m[i];
p[i]+=p[i].back()*b%m[i];
}
}
pair<ll,ll> get(int l,int r){
#define q(i) (a[i][r+1]-a[i][l]*p[i][r-l+1]%m[i]+m[i])%m[i]
return {q(0),q(1)};
#undef q
}
int size(){return a[0].size()-1;}
pair<ll,ll> prefix(int len){return get(0,len-1);}
pair<ll,ll> suffix(int len){return get(size()-len,size()-1);}
}h;
質因數哈希
int fac(int n,int c,int mod,const function<int(int)> &f){
int p=c*c%mod,ans=0;
for(int i=2;i*i<=n;i++){
int cnt=0;
while(n%i==0)n/=i,cnt++;
ans=(ans+p*f(cnt))%mod;
p=p*c%mod;
}
if(n>1)ans=(ans+qpow(c,n,mod)*f(1))%mod;
return ans;
}
//例:匹配乘積為x^k(x任意)的兩個數
pii hash1(int n){
return pii(
fac(n,101,2147483647,[](int x){return x%k;}),
fac(n,103,1000000007,[](int x){return x%k;})
);
}
pii hash2(int n){
return pii(
fac(n,101,2147483647,[](int x){return (k-x%k)%k;}),
fac(n,103,1000000007,[](int x){return (k-x%k)%k;})
);
}
字符串函數
前綴函數×kmp
- \(p[x]\) 表示滿足
s.substr(0,k)==s.substr(x-k,k)
且 \(x\not=k\) 的 \(k\) 的最大值,\(p[0]=0\) - 線性復雜度
int p[N];
void kmp(const string &s){ //求s的前綴函數
p[0]=0; int k=0;
repeat(i,1,s.length()){
while(k>0 && s[i]!=s[k])k=p[k-1];
if(s[i]==s[k])k++;
p[i]=k;
}
}
void solve(string s1,string s2){ //模擬s1.find(s2)
kmp(s2+'#'+s1);
repeat(i,s2.size()+1,s.size())
if(p[i]==(int)s2.size())
ans.push_back(i-2*s2.size()); //編號從0開始的左端點
}
struct KMP{ //kmp自動機
string s; int k;
vector<int> p;
int get(char c){
while(k>0 && c!=s[k])k=p[k-1];
if(c==s[k])k++;
return k;
}
KMP(const string &_s){
p.push_back(k=0);
s=_s+'#'; repeat(i,1,s.size())p.push_back(get(s[i]));
}
int size(){return s.size()-1;}
};
void solve(string s1,string s2){ //模擬s1.find(s2)
KMP kmp(s2);
repeat(i,0,s1.size())
if(kmp.get(s1[i])==kmp.size())
ans.push_back(i+1-kmp.size()); //編號從0開始的左端點
kmp.k=0; //清空(如果下次還要用的話)
}
z函數×exkmp
- \(z[x]\) 表示滿足
s.substr(0,k)==s.substr(x,k)
的 \(k\) 的最大值,\(z[0]=0\) - 線性復雜度
int z[N];
void exkmp(const string &s){ //求s的z函數
fill(z,z+s.size(),0); int l=0,r=0;
repeat(i,1,s.size()){
if(i<=r)z[i]=min(r-i+1,z[i-l]);
while(i+z[i]<(int)s.size() && s[z[i]]==s[i+z[i]])z[i]++;
if(i+z[i]-1>r)l=i,r=i+z[i]-1;
}
}
馬拉車×Manacher
- 預處理為
"#*A*A*A*A*A*"
- 線性復雜度
int len[N*2]; char s[N*2]; //兩倍內存
int manacher(char s1[]){ //s1可以是s
int n=strlen(s1)*2+1;
repeat_back(i,0,n)s[i+1]=(i%2==0?'*':s1[i/2]);
n++; s[0]='#'; s[n++]=0;
len[0]=0;
int mx=0,id=0,ans=0;
repeat(i,1,n-1){
if(i<mx)len[i]=min(mx-i,len[2*id-i]);
else len[i]=1;
while(s[i-len[i]]==s[i+len[i]])len[i]++;
if(len[i]+i>mx)mx=len[i]+i,id=i;
ans=max(ans,len[i]-1); //最長回文串長度
}
return ans;
}
最小表示法
- 求 \(s\) 重復無數次的字符串最小后綴的左端點
- 線性復雜度
int minstr(const string &s){
int k=0,i=0,j=1,n=s.size();
while(max(k,max(i,j))<n){
if(s[(i+k)%n]==s[(j+k)%n])k++;
else{
s[(i+k)%n]>s[(j+k)%n]?i+=k+1:j+=k+1;
if(i==j)i++;
k=0;
}
}
return min(i,j);
}
后綴數組×SA
- \(sa[i]\) 表示所有后綴中第 \(i\) 小的后綴是
s.substr(sa[i],-1)
- \(rk[i]\) 表示所有后綴中
s.substr(i,-1)
是第 \(rk[i]\) 小 - 編號從 $1$ 開始!\(O(n\log n)\)
int sa[N],rk[N]; //sa,rk即結果
void get_sa(const string &S){
static int pre[N*2],id[N],px[N],cnt[N];
int n=S.length(),m=256;
const char *const s=S.c_str()-1; //為了編號從1開始
for(int i=1;i<=n;i++)cnt[rk[i]=s[i]]++;
for(int i=1;i<=m;i++)cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--)sa[cnt[rk[i]]--]=i;
for(int w=1;w<n;w<<=1){
int t=0;
for(int i=n;i>n-w;i--)id[++t]=i;
for(int i=1;i<=n;i++)
if(sa[i]>w)id[++t]=sa[i]-w;
mst(cnt,0);
for(int i=1;i<=n;i++)cnt[px[i]=rk[id[i]]]++;
for(int i=1;i<=m;i++)cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--)sa[cnt[px[i]]--]=id[i];
memcpy(pre,rk,sizeof(rk));
int p=0;
static auto pp=[&](int x){return pii(pre[x],pre[x+w]);};
for(int i=1;i<=n;i++)
rk[sa[i]]=pp(sa[i])==pp(sa[i-1])?p:++p;
m=p; //優化計數排序值域
}
}
- sa可以在同一文本串中在線多次查找模式串(二分查找)
height數組
- 定義 \(lcp(i,j)=\) 后綴 \(i\) 和后綴 \(j\) 的最長公共前綴長度
- 定義 \(height[i]=lcp(sa[i],sa[i-1])\),\(height[1]=0\)
- \(height[rk[i]]\ge height[rk[i-1]]-1\)
- 編號從 $1$ 開始,\(O(n)\)
for(int i=1,k=0;i<=n;i++){
if(k)k--;
while(s[i+k]==s[sa[rk[i]-1]+k])k++;
ht[rk[i]]=k;
}
- 不相等的子串個數為 \(\dfrac{n(n+1)}{2}-\sum\limits_{i=2}^{n}height[i]\)
自動機
字典樹×Trie
- 線性復雜度
struct trie{
int a[N][26],cnt[N],t;
void init(){
t=0; add();
}
int add(){
mst(a[t],0);
cnt[t]=0;
return t++;
}
void insert(const char s[]){
int k=0;
for(int i=0;s[i];i++){
int c=s[i]-'a'; //小寫字母
if(!a[k][c])a[k][c]=add();
k=a[k][c];
//son[k]++; //如果要記錄子樹大小
}
cnt[k]++;
}
int query(const char s[]){
int k=0;
for(int i=0;s[i];i++){
int c=s[i]-'a'; //小寫字母
if(!a[k][c])return 0;
k=a[k][c];
}
return cnt[k];
}
}t;
AC自動機
- 先構建字典樹,再構建fail樹和字典圖
- 線性復雜度
struct AC{
static const int sigma=26,c0='a'; //小寫字母
struct node{
int to[sigma],fail,trie_cnt,cnt;
int &operator[](int x){return to[x];}
//vector<int> p; //指向模式串集合
}a[N];
int t;
vector<int> q; //存了bfs序
void init(){t=0; a[t++]=node(); q.clear();}
void insert(const char s[]/*,int ptr*/){ //將模式串插入字典樹
int k=0;
for(int i=0;s[i];i++){
int c=s[i]-c0;
if(!a[k][c])a[k][c]=t,a[t++]=node();
k=a[k][c];
}
a[k].trie_cnt++;
//a[k].p.push_back(ptr);
}
void build(){ //構建fail樹,將字典樹擴展為圖
int tail=0;
repeat(i,0,sigma)
if(a[0][i])
q.push_back(a[0][i]);
while(tail!=(int)q.size()){
int k=q[tail++];
repeat(i,0,sigma)
if(a[k][i])
a[a[k][i]].fail=a[a[k].fail][i],q.push_back(a[k][i]);
else
a[k][i]=a[a[k].fail][i];
}
}
void query(const char s[]){ //記錄文本串中模式串出現次數
int k=0;
for(int i=0;s[i];i++){
int c=s[i]-c0;
k=a[k][c];
a[k].cnt++; //fail樹上差分
//for(int kk=k;kk!=0;kk=a[kk].fail)
//for(auto p:a[kk].p)
//ans[p]++; //代替下面的反向遍歷,我也不知道什么時候用
}
repeat_back(i,0,q.size()){ //反向遍歷bfs序
a[a[q[i]].fail].cnt+=a[q[i]].cnt; //差分求和
//for(auto p:a[q[i]].p)ans[p]=a[q[i]].cnt; //反饋答案
}
}
}ac;
后綴自動機×SAM
- 給定字符串中所有子串對應了SAM中從源點出發的一條路徑
- SAM是一個DAG,至多有 $2n-1$ 個結點和 $3n-4$ 條邊
- 每個結點表示一個endpos等價類,對應子串長度區間 \([a[a[i].fa].len+1,a[i].len]\)
- 構造 \(O(n)\),編號從 $1$ 開始(\(a[1]\) 表示源點)
struct SAM{
static const int sigma=26,c0='a'; //小寫字母
struct node{
int to[sigma],fa,len;
int &operator[](int x){return to[x];}
}a[N*2];
int last,tot;
void init(){last=tot=1;}
int add(){a[++tot]=node(); return tot;}
void push(int c){
c-=c0;
int x=last,nx=last=add();
a[nx].len=a[x].len+1;
for(;x && a[x][c]==0;x=a[x].fa)a[x][c]=nx;
if(x==0)a[nx].fa=1;
else{
int p=a[x][c];
if(a[p].len==a[x].len+1)a[nx].fa=p;
else{
int np=add();
a[np]=a[p]; a[np].len=a[x].len+1;
a[p].fa=a[nx].fa=np;
for(;x && a[x][c]==p;x=a[x].fa)a[x][c]=np;
}
}
}
}sam;
//構造:for(auto i:s)sam.push(i);
雜項
位運算
位運算巨佬操作
- 中點向下取整
(x+y)/2: (x & y) + ((x ^ y) >> 1)
- 中點向上取整
(x+y+1)/2: (x | y) - ((x ^ y) >> 1)
- 一般來說用
x + (y - x >> 1)
- 一般來說用
abs(n): (n ^ (n >> 31)) - (n >> 31)
max(a,b): b & ((a - b) >> 31) | a & (~(a - b) >> 31)
min(a,b): a & ((a - b) >> 31) | b & (~(a - b) >> 31)
#define B(x,i) ((x>>i)&1) //返回x的第i位
#define Bswap(x,i,j) (B(x,i)^B(x,j)) && (x^=(1ll<<i)|(1ll<<j)) //交換x的i位和j位
#define Bset(x,i,b) (B(x,i)^b) && (x^=(1ll<<i)) //將x的第i位賦值為b
位運算函數
- (不需要頭文件)
__builtin_ctz(x),__builtin_ctzll(x) //返回x后導0的個數,x是0則返回32 or 64
__builtin_clz(x),__builtin_clzll(x) //返回x前導0的個數,x是0則返回32 or 64
__builtin_popcount(x),__builtin_popcountll(x) //返回x中1的個數
__builtin_parity(x),__builtin_parityll(x) //返回x中1的個數是否為奇數
枚舉二進制子集
- 枚舉二進制數m的非空子集
for(int s=m;s;s=(s-1)&m){
work(s);
}
- 枚舉n個元素的大小為k的二進制子集(要保證k不等於0)
int s=(1<<k)-1;
while(s<(1<<n)){
work(s);
int x=s&-s,y=s+x;
s=((s&~y)/x>>1)|y; //這里有一個位反~
}
浮點數
浮點數操作
const lf eps=1e-11;
if(abs(x)<eps)x=abs(x); //輸出浮點數的預處理
浮點數常量
float 1e38, 有效數字6
double 1e308, 有效數字15
long double 1e4932, 有效數字18
常數優化
估計函數用時
clock()
可以獲取時刻,單位毫秒,運行函數前后的時間之差即為用時- 一些巨佬測出來的結論:
- 整數加減:1(個時間單位,下同)
- 整數位運算:1
- 整數乘法:2
- 整數除法:21
- 浮點加減:3
- 浮點除法:35
- 浮點開根:60
快讀快寫
ll read(){
ll x=0,tag=1; char c=getchar();
for(;!isdigit(c);c=getchar())if(c=='-')tag=-1;
for(; isdigit(c);c=getchar())x=x*10+c-48;
return x*tag;
}
void write(ll x){ //可能不比printf快
if(x<0)x=-x,putchar('-');
if(x>=10)write(x/10);
putchar(x%10^48);
}
char getc(){ //代替getchar,用了這個就不能用其他讀入函數如scanf
static char now[1<<16],*S,*T;
if(T==S){T=(S=now)+fread(now,1,1<<16,stdin); if(T==S)return EOF;}
return *S++;
}
STL手寫內存分配器
static char space[10000000],*sp=space;
template<typename T>
struct allc:allocator<T>{
allc(){}
template<typename U>
allc(const allc<U> &a){}
template<typename U>
allc<T>& operator=(const allc<U> &a){return *this;}
template<typename U>
struct rebind{typedef allc<U> other;};
T* allocate(size_t n){
T *res=(T*)sp;
sp+=n*sizeof(T);
return res;
}
void deallocate(T* p,size_t n){}
};
vector< int,allc<int> > a;
吸氧氣
#pragma GCC optimize(2) //(3),("Ofast")
其他優化
//(都是聽說的)
1ll*a 比 (ll)a 快
取模:x%mod 優化為 x<mod?x:x%mod
減少浮點除法:a/b+c/d 優化為 (a*d+b*c)/(b*d)
精度足夠時用ll代替浮點類型
多路並行運算,如 (a+b)+(c+d) 比 ((a+b)+c)+d 快
加上inline,以及強制內聯__inline __attribute__((always_inline))
多重for循環時,修改for的順序保證內存連續訪問
多使用局部變量
在TLE邊緣試探
while(clock()<0.9*CLOCKS_PER_SEC){
//反復更新最優解
}
對拍
#include<bits/stdc++.h>
using namespace std;
int main(){
for(int i=0;;i++){
if(i%10==0)cerr<<i<<endl;
system("gen.exe > test.in");
system("test1.exe < test.in > a.out");
system("test2.exe < test.in > b.out");
if(system("fc a.out b.out")){
system("pause");
return 0;
}
}
}
備選
#include<bits/stdc++.h>
using namespace std;
ifstream a,b;
int main(){
for(int i=0;;i++){
if(i%10==0)cerr<<i<<endl;
system("datamaker.exe > data.txt");
system("A.exe < data.txt > a.out");
system("B.exe < data.txt > b.out");
a.open("a.out");
b.open("b.out");
while(a.good() || b.good()){
if(a.get()!=b.get()){
system("pause");
return 0;
}
}
a.close(),b.close();
}
}
戰術分析
(我真的真的真的太南了)
- 可能造成錯誤的注意點
ll t; 1<<t返回int,必須是1ll<<t
int x; x<<y的y會先對32取模
operator<的比較內容一定要寫完整
試一試輸入^Z能否結束
無向圖輸入要給兩個值賦值g[x][y]=g[x][y]=1
多組輸入時,圖記得初始化
建模的轉換函數的宏定義一定要加括號,或者寫成函數
多想想極端數據!!
islower()等函數返回值不一定是0或1
多用相空間角度思考問題
內存比我想象的要大一些(有時候1e7可以塞下)
在64位編譯器(我的編譯器)中set每個元素需要額外32字節內存
struct里放大數組,最好用vector代替
數學
數論
基本操作
模乘 模冪 模逆 擴歐
ll mul(ll a,ll b,ll m=mod){return a*b%m;} //模乘
ll qpow(ll a,ll b,ll m=mod){ //快速冪
ll ans=1;
for(;b;a=mul(a,a,m),b>>=1)
if(b&1)ans=mul(ans,a,m);
return ans;
}
void exgcd(ll a,ll b,ll &d,ll &x,ll &y){ //擴歐,ax+by=gcd(a,b),d存gcd
if(!b)d=a,x=1,y=0;
else exgcd(b,a%b,d,y,x),y-=x*(a/b);
}
ll gcdinv(ll v,ll m=mod){ //擴歐版逆元
ll d,x,y;
exgcd(v,m,d,x,y);
return (x%m+m)%m;
}
ll getinv(ll v,ll m=mod){ //快速冪版逆元,m必須是質數!!
return qpow(v,m-2,m);
}
ll qpows(ll a,ll b,ll m=mod){
if(b>=0)return qpow(a,b,m);
else return getinv(qpow(a,-b,m),m);
}
防爆模乘
//int128版本
ll mul(ll a,ll b,ll m=mod){return (__int128)a*b%m;}
//llf版本(欲防爆,先自爆)(注意在測試的時候不知道為什么有鍋)
ll mul(ll a,ll b,ll m=mod){return (a*b-ll((llf)a/m*b)*m+m)%m;}
//每位運算一次版本,注意這是真·龜速乘,O(logn)
ll mul(ll a,ll b,ll m=mod){
ll ans=0;
while(b){
if(b&1)ans=(ans+a)%m;
a=(a+a)%m;
b>>=1;
}
return ans;
}
//把b分成兩部分版本,要保證m小於1<<42(約等於4e12),a,b<m
ll mul(ll a,ll b,ll m=mod){
a%=m,b%=m;
ll l=a*(b>>21)%m*(1ll<<21)%m;
ll r=a*(b&(1ll<<21)-1)%m;
return (l+r)%m;
}
最大公約數
__gcd(a,b) //內置gcd,推薦
ll gcd(ll a,ll b){return b==0?a:gcd(b,a%b);} //不推薦233,比內置gcd慢
ll gcd(ll a,ll b){ //卡常gcd來了!!
#define tz __builtin_ctzll
if(!a || !b)return a|b;
int t=tz(a|b);
a>>=tz(a);
while(b){
b>>=tz(b);
if(a>b)swap(a,b);
b-=a;
}
return a<<t;
#undef tz
}
- 實數gcd
lf fgcd(lf a,lf b){return abs(b)<1e-5?a:fgcd(b,fmod(a,b));}
高級模操作
同余方程組 | CRT+extra
//CRT,m[i]兩兩互質
ll crt(ll a[],ll m[],int n){ //ans%m[i]==a[i]
repeat(i,0,n)a[i]%=m[i];
ll M=1,ans=0;
repeat(i,0,n)
M*=m[i];
repeat(i,0,n){
ll k=M/m[i],t=gcdinv(k%m[i],m[i]); //擴歐!!
ans=(ans+a[i]*k*t)%M; //兩個乘號可能都要mul
}
return (ans+M)%M;
}
//exCRT,m[i]不需要兩兩互質,基於擴歐exgcd和龜速乘mul
ll excrt(ll a[],ll m[],int n){ //ans%m[i]==a[i]
repeat(i,0,n)a[i]%=m[i]; //根據情況做適當修改
ll M=m[0],ans=a[0],g,x,y; //M是m[0..i]的最小公倍數
repeat(i,1,n){
ll c=((a[i]-ans)%m[i]+m[i])%m[i];
exgcd(M,m[i],g,x,y); //Ax=c(mod B)
if(c%g)return -1;
ans+=mul(x,c/g,m[i]/g)*M; //龜速乘
M*=m[i]/g;
ans=(ans%M+M)%M;
}
return (ans+M)%M;
}
離散對數 | BSGS+extra
- 求 \(a^x \equiv b \pmod m\) ,\(O(\sqrt m)\)
//BSGS,a和mod互質
ll bsgs(ll a,ll b,ll mod){ //a^ans%mod==b
a%=mod,b%=mod;
static unordered_map<ll,ll> m; m.clear();
ll t=(ll)sqrt(mod)+1,p=1;
repeat(i,0,t){
m[mul(b,p,mod)]=i; //p==a^i
p=mul(p,a,mod);
}
a=p; p=1;
repeat(i,0,t+1){
if(m.count(p)){ //p==a^i
ll ans=t*i-m[p];
if(ans>0)return ans;
}
p=mul(p,a,mod);
}
return -1;
}
//exBSGS,a和mod不需要互質,基於BSGS
ll exbsgs(ll a,ll b,ll mod){ //a^ans%mod==b
a%=mod,b%=mod;
if(b==1)return 0;
ll ans=0,c=1,g;
while((g=__gcd(a,mod))!=1){
if(b%g!=0)return -1;
b/=g,mod/=g;
c=mul(c,a/g,mod);
ans++;
if(b==c)return ans;
}
ll t=bsgs(a,mul(b,getinv(c,mod),mod),mod); //必須擴歐逆元!!
if(t==-1)return -1;
return t+ans;
}
階與原根
- 判斷是否有原根:若 \(m\) 有原根,則 \(m\) 一定是下列形式:$2,4,pa,2pa$( \(p\) 是奇素數, \(a\) 是正整數)
- 求所有原根:若 \(g\) 為 \(m\) 的一個原根,則 \(g^s\space(1\le s\le\varphi(m),\gcd(s,\varphi(m))=1)\) 給出了 \(m\) 的所有原根。因此若 \(m\) 有原根,則 \(m\) 有 \(\varphi(\varphi(m))\) 個原根
- 求一個原根,\(O(n\log\log n)\)
實際遠遠不到
ll getG(ll n){ //求n最小的原根
static vector<ll> a; a.clear();
ll t=0,k=n-1;
repeat(i,2,sqrt(k+1)+1)
if(k%i==0){
a.push_back(i); //a存放(n-1)的質因數
while(k%i==0)k/=i;
}
if(k!=1)a.push_back(k);
repeat(i,2,n){ //枚舉答案
bool f=1;
for(auto j:a)
if(qpow(i,(n-1)/j,n)==1){
f=0;
break;
}
if(f)return i;
}
}
N次剩余
- 求 \(x^a \equiv b \pmod m\) ,基於BSGS、原根
//只求一個
ll residue(ll a,ll b,ll mod){ //ans^a%mod==b
ll g=getG(mod),c=bsgs(qpow(g,a,mod),b,mod);
if(c==-1)return -1;
return qpow(g,c,mod);
}
//求所有N次剩余
vector<ll> ans;
void allresidue(ll a,ll b,ll mod){ //ans^a%mod==b
ll g=getG(mod),c=bsgs(qpow(g,a,mod),b,mod);
ans.clear();
if(b==0){ans.push_back(0);return;}
if(c==-1)return;
ll now=qpow(g,c,mod);
ll step=(mod-1)/__gcd(a,mod-1);
ll ps=qpow(g,step,mod);
for(ll i=c%step;i<mod-1;i+=step,now=mul(now,ps,mod))
ans.push_back(now);
sort(ans.begin(),ans.end());
}
數論函數的生成
單個歐拉函數
- \(\varphi(n)=\) 小於
n
且與n
互質的正整數個數 - 令
n
的唯一分解式 \(n=Π({p_k}^{a_k})\),則 \(\varphi(n)=n\cdot Π(1-\dfrac 1 {p_k})\) - \(O(\sqrt n)\)
int getphi(int n){
int ans=n;
repeat(i,2,sqrt(n)+2)
if(n%i==0){
while(n%i==0)n/=i;
ans=ans/i*(i-1);
}
if(n>1)ans=ans/n*(n-1);
return ans;
}
線性遞推乘法逆元
求1..(n-1)的逆元,\(O(n)\)
void get_inv(int n,int m=mod){
inv[1]=1;
repeat(i,2,n)inv[i]=m-m/i*inv[m%i]%m;
}
求a[1..n]的逆元,離線,\(O(n)\)
void get_inv(int a[],int n){ //求a[1..n]的逆元,存在inv[1..n]中
static int pre[N];
pre[0]=1;
repeat(i,1,n+1)
pre[i]=(ll)pre[i-1]*a[i]%mod;
int inv_pre=qpow(pre[n],mod-2);
repeat_back(i,1,n+1){
inv[i]=(ll)pre[i-1]*inv_pre%mod;
inv_pre=(ll)inv_pre*a[i]%mod;
}
}
線性篩
- 定理:求出 \(f(p)\)(p為質數)的復雜度不超過 \(O(\log p)\) 的積性函數可以被線性篩
篩素數
- \(O(n)\)
//a[i]表示第i+1個質數,vis[i]==0表示i是素數,rec[i]為i的最小質因數
void get_prime(int n){
int cnt=0; vis[1]=1;
repeat(i,2,n+1){
if(!vis[i])
a[cnt++]=i; //,rec[i]=i;
repeat(j,0,cnt){
if(i*a[j]>n)break;
vis[i*a[j]]=1; //rec[i*a[j]]=a[j];
if(i%a[j]==0)break;
}
}
}
篩歐拉函數
- 線性版,\(O(n)\)
void get_phi(){
int cnt=0; /*vis[1]=1;*/ phi[1]=1;
repeat(i,2,n+1){
if(!vis[i])
a[cnt++]=i,phi[i]=i-1;
repeat(j,0,cnt){
if(i*a[j]>n)break;
vis[i*a[j]]=1;
if(i%a[j]==0){
phi[i*a[j]]=phi[i]*a[j];
break;
}
phi[i*a[j]]=phi[i]*(a[j]-1);
}
}
}
- 不是線性但節省力氣和空間版,\(O(n\log\log n)\)
void get_phi(){
phi[1]=1; //其他的值初始化為0
repeat(i,2,n+1)
if(!phi[i])
for(int j=i;j<=n;j+=i){
if(!phi[j])phi[j]=j;
phi[j]=phi[j]/i*(i-1);
}
}
篩莫比烏斯函數
- \(O(n)\)
void get_mu(int n){
int cnt=0; /*vis[1]=1;*/ mu[1]=1;
repeat(i,2,n+1){
if(!vis[i])a[cnt++]=i,mu[i]=-1;
repeat(j,0,cnt){
if(i*a[j]>n)break;
vis[i*a[j]]=1;
if(i%a[j]==0){mu[i*a[j]]=0; break;}
mu[i*a[j]]=-mu[i];
}
}
}
篩gcd
- \(O(nm)\)
int gcd[N][N];
void get_gcd(int n,int m){
repeat(i,1,n+1)
repeat(j,1,m+1)
if(!gcd[i][j])
repeat(k,1,min(n/i,m/j)+1)
gcd[k*i][k*j]=k;
}
杜教篩
- \(S(n)=\sum\limits_{i=1}^nf(i)\)
- 由公式
- \(\sum\limits_{i=1}^n\sum\limits_{d\mid i}g(d)f(\dfrac i d)=\sum\limits_{i=1}^n g(i)S(\lfloor\dfrac n i \rfloor)\)
- 右式拿出一項得
- \(g(1)S(n)=\sum\limits_{i=1}^n\sum\limits_{d\mid i}g(d)f(\dfrac i d)-\sum\limits_{i=2}^n g(i)S(\lfloor\dfrac n i \rfloor)\)
- 如果能找到合適的 \(g(n)\) (能快速計算 \(\sum\limits_{i=1}^n\sum\limits_{d\mid i}g(d)f(\dfrac i d)\))就能快速算出
- 目前只有mu的杜教篩,\(O(n^{\tfrac 2 3})\)
struct DU{
static const int N=2000010;
map<ll,ll> mp_mu;
int sum_mu[N],a[N],mu[N];
bool vis[N];
ll S_mu(ll x){ //求mu的前綴和
if(x<N)return sum_mu[x];
if(mp_mu[x])return mp_mu[x];
ll ret=1;
for(ll i=2,j;i<=x;i=j+1){
j=x/(x/i);
ret-=S_mu(x/i)*(j-i+1);
}
return mp_mu[x]=ret;
}
void init(){
int cnt=0; /*vis[1]=1;*/ mu[1]=1;
repeat(i,2,N){
if(!vis[i])a[cnt++]=i,mu[i]=-1;
repeat(j,0,cnt){
if(i*a[j]>=N)break;
vis[i*a[j]]=1;
if(i%a[j]==0){
mu[i*a[j]]=0;
break;
}
mu[i*a[j]]=-mu[i];
}
}
repeat(i,1,N)
sum_mu[i]=sum_mu[i-1]+mu[i];
}
}du;
素數約數相關
唯一分解
- 用數組表示數字唯一分解式的素數的指數,如 $50={1,0,2,0,…}$
- 可以用來計算階乘和乘除操作
void fac(int a[],ll n){
repeat(i,2,(int)sqrt(n)+2)
while(n%i==0)a[i]++,n/=i;
if(n>1)a[n]++;
}
- set維護版
struct fac{
#define facN 1010
ll a[facN]; set<ll> s;
fac(){mst(a,0); s.clear();}
void lcm(ll n){ //self=lcm(self,n)
repeat(i,2,facN)
if(n%i==0){
ll cnt=0;
while(n%i==0)cnt++,n/=i;
a[i]=max(a[i],cnt); //改成a[i]+=cnt就變成了乘法
}
if(n>1)s.insert(n);
}
ll value(){ //求自己的模意義下的值
ll ans=1;
repeat(i,2,facN)
if(a[i])ans=ans*qpow(i,a[i],mod)%mod;
for(auto i:s)ans=ans*i%mod;
return ans;
}
}f;
素數判定 | 朴素 or Miller-Rabin
- 朴素算法,\(O(\sqrt n)\)
bool isprime(int n){
if(n<=3)return n>=2;
if(n%2==0 || n%3==0)return 0;
repeat(i,1,int(sqrt(n)+1.5)/6+1)
if(n%(i*6-1)==0 || n%(i*6+1)==0)return 0;
return 1;
}
- Miller-Rabin素性測試,\(O(10\cdot\log^3 n)\)
bool isprime(ll n){
if(n<4)return n>1;
ll a=n-1,b=0;
while(a%2==0)a/=2,++b;
repeat(i,0,10){
ll x=rnd()%(n-2)+2,v=qpow(x,a,n);
if(v==1 || v==n-1)continue;
repeat(j,0,b+1){
v=mul(v,v,n); //mul要防爆
if(v==n-1)break;
}
if(v!=n-1)return 0;
}
return 1;
}
大數分解 | Pollard-rho
- \(O(n^{\tfrac 1 4})\),基於MR素性測試(很遺憾的是,我不擅長卡常因此這個板子過不了洛谷P4718)
ll pollard_rho(ll c,ll n){
ll i=1,x,y,k=2,d;
x=y=rnd()%n;
while(1){
d=__gcd(n+y-x,n);
if(d>1 && d<n)
return d;
if(++i==k)y=x,k*=2;
x=(mul(x,x,n)+n-c)%n; //mul要防爆
if(y==x)return n;
}
}
vector<ll> ans; //存結果(質因數,無序)
void rho(ll n){ //分解n
if(isprime(n)){
ans.push_back(n);
return;
}
ll t;
do{t=pollard_rho(rnd()%(n-1)+1,n);}while(t>=n);
rho(t);
rho(n/t);
}
單個約數個數函數
int get_divisor(int n){ //求約數個數
int ans=0;
for(int i=1;i<n;i=n/(n/(i+1)))
if(n%i==0)
ans++; //v.push_back(i); //記錄約數
return ans+1; //v.push_back(n); //記錄約數
}
反素數生成
- 求因數最多的數(因數個數一樣則取最小)
- 性質:\(M = {p_1}^{k_1}{p_2}^{k_2}...\) 其中,\(p_i\) 是從 $2$ 開始的連續質數,\(k_i-k_{i+1}∈\{0,1\}\)
- 先打出質數表再 \(dfs\),枚舉 \(k_n\),\(O(\exp)\)
int pri[16]={2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53};
ll n; //范圍
pair<ll,ll> ans; //ans是結果,ans.fi是最大反素數,ans.se是反素數約數個數
void dfs(ll num=1,ll cnt=1,int *p=pri,int pre=inf){ //注意ans要初始化
if(make_pair(cnt,-num)>make_pair(ans.se,-ans.fi))
ans={num,cnt};
num*=*p;
for(int i=1;i<=pre && num<=n;i++,num*=*p)
dfs(num,p+1,i,cnt*(i+1));
}
\(n\) 以內約數個數最大值是 \(O(n^{\tfrac {1.066}{\ln\ln n}})\)
范圍 | 1e4 | 1e5 | 1e6 | 1e9 | 1e16 |
---|---|---|---|---|---|
最大反素數 | 7560 | 83160 | 720720 | 735134400 | 8086598962041600 |
反素數約數個數 | 64 | 128 | 240 | 1344 | 41472 |
數論雜項
數論分塊
\(n=(k-n\%k)(n/k)+(n\%k)(n/k+1)\) 將 \(\lfloor \dfrac{n}{x}\rfloor=C\) 的 \([x_{\min},x_{\max}]\) 作為一塊,其中區間內的任一整數 \(x_0\) 滿足 \(x_{\max}=n/(n/x_0)\)
for(int l=l0,r;l<=r0;l=r+1){
r=min(r0,n/(n/l));
//c=n/l;
//len=l-r+1;
}
將 \(\lceil \dfrac{n}{x}\rceil=C\) 的 \([x_{\min},x_{\max}]\) 作為一塊:
for(int l=l0,r;l<=r0;l=r+1){
r=min(r0,n/(n/l)); if(n%r==0)r=max(r-1,l);
//c=(n+l-1)/l;
//len=l-r+1;
}
二次剩余
- 對於奇素數模數 \(p\),存在 \(\frac {p-1} 2\) 個二次剩余 \(\{1^2,2^2,...,(\frac {p-1} 2)^2\}\),和相同數量的二次非剩余
- 對於奇素數模數 \(p\),如果 \(n^{\frac{p-1}2}\equiv1\pmod{p}\) ,則 \(n\) 是一個二次剩余;如果 \(n^{\frac{p-1}2}\equiv-1\pmod{p}\),則 \(n\) 是一個二次非剩余
- 對於奇素數模數 \(p\),二次剩余的乘積是二次剩余,二次剩余與非二次剩余乘積為非二次剩余,非二次剩余乘積是二次剩余
- 費馬-歐拉素數定理:\((4n+1)\) 型素數只能用一種方法表示為一個范數(兩個完全平方數之和),\((4n+3)\) 型素數不能表示為一個范數
- 二次互反率:記 \(p^{\frac{q-1}2}\) 的符號為 \((\dfrac p q)\) ,則對奇素數 \(p,q\) 有 \((\dfrac p q)\cdot(\dfrac q p)=(-1)^{\tfrac{p-1}2\cdot\tfrac{q-1}2}\)
莫比烏斯反演
- 記$[P]=\begin 1&命題P為真\0&命題P為假\end$
- 引理1:\(\lfloor \dfrac{a}{bc}\rfloor=\lfloor \dfrac{\lfloor \dfrac{a}{b}\rfloor}{c}\rfloor\)
- 引理2:\(n\) 的因數個數 \(≤\lfloor 2\sqrt n \rfloor\)
- 數論分塊:將 \(\lfloor \dfrac{n}{x}\rfloor=C\) 的 \([x_{\min},x_{\max}]\) 作為一塊,其中區間內的任一整數 \(x_0\) 滿足 \(x_{\max}=\lfloor\dfrac n{\lfloor\tfrac n x\rfloor}\rfloor\)
- def-積性函數:\(\gcd(x,y)=1\Rightarrow f(xy)=f(x)f(y)\)
- def-單位函數:\(\varepsilon(n)=[n=1]\)
- def-恆等函數:\(id(n)=n\)
- def-狄利克雷Dirichlet卷積:\((f*g)(n)=\sum\limits_{d|n}f(d)g(\dfrac n d)\)
- \(\varepsilon\) 為狄利克雷卷積的單位元
- 莫比烏斯函數性質:\(\mu(n)=\begin{cases} 1&n=1\\0&n含有平方因子\\(-1)^k&k為n的本質不同質因數個數\end{cases}\)
- 莫比烏斯函數是積性函數
- 超級重要結論:\(\varepsilon=\mu*1\),即$\varepsilon(n)=\sum\limits_{d|n}\mu(d)$
- 莫比烏斯反演公式:若$f=g1$,則$g=f\mu$;或者,若$f(n)=\sum\limits_{d|n}g(d)$,則$g(n)=\sum\limits_{d|n}\mu(d)f(\dfrac n d)$
- (補充)歐拉函數性質:\(\varphi*1=id\)
- 例題:求模意義下的 \(\sum\limits_{i=1}^n \sum\limits_{j=1}^m \dfrac{i\cdot j}{\gcd(i,j)}\)
- \(ans=\sum\limits_{i=1}^n\sum\limits_{j=1}^m\sum\limits_{d|i,d|j,\gcd(\frac i d,\frac j d)=1}\dfrac{i\cdot j}d\)
- 非常經典的化法:
- \(ans=\sum\limits_{d=1}^n d\cdot\sum\limits_{i=1}^{\lfloor\frac nd\rfloor}\sum\limits_{j=1}^{\lfloor\frac md\rfloor}[\gcd(i,j)=1]i\cdot j\)
- 設 \(sum(n,m)=\sum\limits_{i=1}^{n}\sum\limits_{j=1}^{m}[\gcd(i,j)=1]i\cdot j\)
- \(sum(n,m)=\sum\limits_{i=1}^{n}\sum\limits_{j=1}^{m}\sum\limits_{c|i,c|j}{\mu(c)}\cdot i\cdot j\)
- 設 \(i'=\dfrac i c,j'=\dfrac j c\)
- \(sum(n,m)=\sum\limits_{c=1}^n\mu(c)\cdot c^2\cdot\sum\limits_{i'=1}^{\lfloor\frac nc\rfloor}\sum\limits_{j'=1}^{\lfloor\frac mc\rfloor} i'\cdot j'\)
- 易得 \(\sum\limits_{i=1}^{n}\sum\limits_{j=1}^{m} i\cdot j=\dfrac 1 4 n(n+1) m(m+1)\)
斐波那契數列
- 定義:\(F_0=0,F_1=1,F_n=F_{n-1}+F_{n-2}\)
- \(F_n=\dfrac 1 {\sqrt{5}} [(\dfrac{1+\sqrt 5}2)^n-(\dfrac{1-\sqrt 5}2)^n)]\) (公式中若 $5$ 是二次剩余則可以化簡,比如 \(\sqrt 5\equiv 383008016\pmod {1000000009}\))
- \(F_{a+b-1}=F_{a-1}F_{b-1}+F_aF_b\) (重要公式)
- \(F_{n-1}F_{n+1}-F_n^2=(-1)^n\) (卡西尼性質)
- \(F_{n}^2+F_{n+1}^2=F_{2n+1}\)
- \(F_{n+1}^2-F_{n-1}^2=F_{2n}\) (由上一條寫兩遍相減得到)
- \(F_1+F_3+F_5+...+F_{2n-1}=F_{2n}\) (奇數項求和)
- \(F_2+F_4+F_6+...+F_{2n}=F_{2n+1}-1\) (偶數項求和)
- \(F_1^2+F_2^2+F_3^2+...+F_n^2=F_nF_{n+1}\)
- \(F_1+2F_2+3F_3+...+nF_n=nF_{n+2}-F_{n+3}+2\)
- \(-F_1+F_2-F_3+...+(-1)^nF_n=(-1)^n(F_{n+1}-F_n)+1\)
- \(F_{2n-2m-2}(F_{2n}+F_{2n+2})=F_{2m+2}+F_{4n-2m}\)
- \(F_a \mid F_b \Leftrightarrow a \mid b\)
- \(\gcd(F_a,F_b)=F_{\gcd(a,b)}\)
- 當 \(p\) 為 $5k\pm 1$ 型素數時,\(\begin{cases} F_{p-1}\equiv 0\pmod p \\ F_p\equiv 1\pmod p \\ F_{p+1}\equiv 1\pmod p \end{cases}\)
- 當 \(p\) 為 $5k\pm 2$ 型素數時,\(\begin{cases} F_{p-1}\equiv 1\pmod p \\ F_p\equiv -1\pmod p \\ F_{p+1}\equiv 0\pmod p \end{cases}\)
- \(F_{n+2}\) 為集合
{1,2,3,...,n-2}
中不包含相鄰正整數的子集個數(包括空集) F(n)%m
的周期 \(\le 6m\) ( \(m=2\times 5^k\) 取等號)
快速倍增法求$F_n$,返回二元組$(F_n,F_{n+1})$ ,\(O(\log n)\)
pii fib(ll n){ //fib(n).fi即結果
if(n==0)return {0,1};
pii p=fib(n>>1);
ll a=p.fi,b=p.se;
ll c=a*(2*b-a)%mod;
ll d=(a*a+b*b)%mod;
if(n&1)return {d,(c+d)%mod};
else return {c,d};
}
組合數學
組合數取模 | Lucas+extra
Lucas定理用來求模意義下的組合數
真·Lucas,\(p\) 是質數(后面的exLucas都不純正)
ll lucas(ll a,ll b,ll p){ //a>=b
if(b==0)return 1;
return mul(c(a%p,b%p,p),lucas(a/p,b/p,p),p);
}
特例:如果p=2,可能lucas失效(?)
ll C(ll a,ll b){ //a>=b,p=2的情況
return (a&b)==b;
}
快速階乘和exLucas
- \(qfac.A(x),qfac.B(x)\) 滿足 \(A\equiv \dfrac{x!}{p^B}\pmod {p^k}\)
- \(qfac.C(a,b)\equiv C_a^b \pmod {p^k}\)
- \(exlucas(a,b,m)\equiv C_a^b \pmod m\),函數內嵌中國剩余定理
struct Qfac{
ll s[2000010];
ll p,m;
ll A(ll x){ //快速階乘的A值
if(x==0)return 1;
ll c=A(x/p);
return s[x%m]*qpow(s[m],x/m,m)%m*c%m;
}
ll B(ll x){ //快速階乘的B值
int ans=0;
for(ll i=x;i;i/=p)ans+=i/p;
return ans;
}
ll C(ll a,ll b){ //組合數,a>=b
ll k=B(a)-B(b)-B(a-b);
return A(a)*gcdinv(A(b),m)%m
*gcdinv(A(a-b),m)%m
*qpow(p,k,m)%m;
}
void init(ll _p,ll _m){ //初始化,一定要滿足m=p^k
p=_p,m=_m;
s[0]=1;
repeat(i,1,m+1)
if(i%p)s[i]=s[i-1]*i%m;
else s[i]=s[i-1];
}
}qfac;
ll exlucas(ll a,ll b,ll mod){
ll ans=0,m=mod;
for(ll i=2;i<=m;i++) //不能repeat
if(m%i==0){
ll p=i,k=1;
while(m%i==0)m/=i,k*=i;
qfac.init(p,k);
ans=(ans+qfac.C(a,b)*(mod/k)%mod*gcdinv(mod/k,k)%mod)%mod;
}
return (ans+mod)%mod;
}
線性遞推組合數
- \(O(n)\) 初始化,\(O(1)\) 查詢
struct CC{
static const int N=100010;
ll fac[N],inv[N];
CC(){
fac[0]=1;
repeat(i,1,N)fac[i]=fac[i-1]*i%mod;
inv[N-1]=qpow(fac[N-1],mod-2,mod);
repeat_back(i,0,N-1)inv[i]=inv[i+1]*(i+1)%mod;
}
ll operator()(ll a,ll b){ //a>=b
if(a<b)return 0;
return fac[a]*inv[a-b]%mod*inv[b]%mod;
}
}C;
康托展開+逆 編碼與解碼
康托展開+逆
- 康托展開即排列到整數的映射
- 排列里的元素都是從1到n
//普通版,O(n^2)
int cantor(int a[],int n){
int f=1,ans=1; //假設答案最小值是1
repeat_back(i,0,n){
int cnt=0;
repeat(j,i+1,n)cnt+=a[j]<a[i];
ans=(ans+f*cnt%mod)%mod; //ans+=f*cnt;
f=f*(n-i)%mod; //f*=(n-i);
}
return ans;
}
//樹狀數組優化版,基於樹狀數組,O(nlogn)
int cantor(int a[],int n){
static BIT t; t.init(); //樹狀數組
ll f=1,ans=1; //假設答案最小值是1
repeat_back(i,0,n){
ans=(ans+f*t.sum(a[i])%mod)%mod; //ans+=f*t.sum(a[i]);
t.add(a[i],1);
f=f*(n-i)%mod; //f*=(n-i);
}
return ans;
}
//逆展開普通版,O(n^2)
int *decantor(int x,int n){
static int f[13]={1};
repeat(i,1,13)f[i]=f[i-1]*i;
static int ans[N];
set<int> s;
x--;
repeat(i,1,n+1)s.insert(i);
repeat(i,0,n){
int q=x/f[n-i-1];
x%=f[n-i-1];
auto it=s.begin();
repeat(i,0,q)it++; //第q+1小的數
ans[i]=*it;
s.erase(it);
}
return ans;
}
編碼與解碼問題
<1>
- 給定一個字符串,求出它的編號
- 例,輸入acab,輸出5(aabc,aacb,abac,abca,acab,...)
- 用遞歸,令d(S)是小於S的排列數,f(S)是S的全排列數
- 小於acab的第一個字母只能是a,所以d(acab)=d(cab)
- 第二個字母是a,b,c,所以d(acab)=f(bc)+f(ac)+d(ab)
- d(ab)=0
- 因此d(acab)=4,加1之后就是答案
<2>
- 給定編號求字符串,對每一位進行嘗試即可
置換群計數
Polya定理
- 例:立方體 \(n=6\) 個面,每個面染上 \(m=3\) 種顏色中的一種
- 兩個染色方案相同意味着兩個立方體經過旋轉可以重合
- 其染色方案數為:\(\dfrac{\sum m^{k_i}}{|k|}\)(\(k_i\) 為某一置換可以拆分的循環置換數,\(|k|\) 為所有置換數)
不旋轉,{U|D|L|R|F|B},k=6,共1個
對面中心連線為軸的90度旋轉,{U|D|L R F B},k=3,共6個
對面中心連線為軸的180度旋轉,{U|D|L R|F B},k=4,共3個
對棱中點連線為軸的180度旋轉,{U L|D R|F B},k=3,共6個
對頂點連線為軸的120度旋轉,{U L F|D R B},k=2,共8個
- 因此 \(\dfrac{3^6+3^3 \cdot 6+3^4 \cdot 3+3^3 \cdot 6+3^2 \cdot 8}{1+6+3+6+8}=57\)
- 例題(poj1286),n個點連成環,染3種顏色,允許旋轉和翻轉
ans=0,cnt=0;
//只考慮旋轉,不考慮翻轉
repeat(i,1,n+1)
ans+=qpow(3,__gcd(i,n));
cnt+=n;
//考慮翻轉
if(n%2==0)ans+=(qpow(3,n/2+1)+qpow(3,n/2))*(n/2);
else ans+=qpow(3,(n+1)/2)*n;
cnt+=n;
cout<<ans/cnt<<endl;
組合數學的一些結論
組合數
C(n,k)=(n-k+1)*C(n,k-1)/k
const int N=20;
repeat(i,0,N){
C[i][0]=C[i][i]=1;
repeat(j,1,i)C[i][j]=C[i-1][j]+C[i-1][j-1];
}
- 卡塔蘭/卡特蘭數×Catalan,\(H_n=\dfrac{\binom{2n}n}{n+1}\),\(H_n=\dfrac{H_{n-1}(4n-2)}{n+1}\)
- 貝爾數×Bell,划分n個元素的集合的方案數
B[0]=B[1]=1;
repeat(i,2,N){
B[i]=0;
repeat(j,0,i)
B[i]=(B[i]+C(i-1,j)*B[j]%mod)%mod;
}
- 錯排數,\(D_n=n![\dfrac 1{0!}-\dfrac 1{1!}+\dfrac 1{2!}-...+\dfrac{(-1)^n}{n!}]\)
D[0]=1;
repeat(i,0,N-1){
D[i+1]=D[i]+(i&1?C.inv[i+1]:mod-C.inv[i+1]);
D[i]=1ll*D[i]*fac[i]%mod;
}
第一類斯特林數×Stirling
- 多項式 \(x(x-1)(x-2) \cdots (x-n+1)\) 展開后 \(x^r\) 的系數絕對值記作 \(s(n,r)\) (系數符號 \((-1)^{n+r}\))
- 遞推式 \(s(n,r) = (n-1)s(n-1,r)+s(n-1,r-1)\)
第二類斯特林數×Stirling
- \(n\) 個不同的球放入 \(r\) 個相同的盒子且無空盒,記作 \(S(n,r)\) 或 \(S_n^r\)
- 遞推式 \(S(n,r) = r S(n-1,r) + S(n-1,r-1)\)
- 通項公式 \(S(n,r) = \dfrac 1 {r!}\sum\limits_{k=0}^{r}C_r^k(r-k)^n=\sum\limits_{k=0}^{r}\dfrac{(-1)^k(r-k)^n}{k!(r-k)!}\)
- 一個長為 \(n+m\) 的數組,\(n\) 個 $1$,\(m\) 個 \(-1\),限制前綴和最大為 \(k\),則方案數為 \(C_{n+m}^{m+k}-C_{n+m}^{m+k+1}\)
博弈論
SG定理
- 有向無環圖中,兩個玩家輪流推一個棋子,不能走的判負
- 假設x的k個后繼狀態
{yi}
SG(x)=mex{SG(yi)}
,mex(S)=
不屬於集合S的最小自然數- 當且僅當
XOR{SG(起點1),SG(起點2),...}
為0時先手必敗 - (如果只有一個起點,SG的值可以只考慮01)
- 例題:拿n堆石子,每次只能拿一堆中的斐波那契數顆石子
void getSG(int n){
mst(SG,0);
repeat(i,1,n+1){
mst(S,0);
for(int j=0;f[j]<=i && j<=N;j++)
S[SG[i-f[j]]]=1;
for(int j=0;;j++)
if(!S[j]){
SG[i]=j;
break;
}
}
}
Nim
- n堆石子,分別有
ai
顆石子 - 兩人每次可以拿任意堆任意非空的石子,拿不了的人判負
- 解:
NimSum=XOR{ai}
,當且僅當NimSum
為0時先手必敗(SG證) - 注:先手必勝策略是找到任意第
(63-__builtin_clzll(NimSum))
位是1的a[i],並取走a[i]^NimSum
個石子
Nimk
- 每次最多選取k堆石子,選中的每一堆都取走任意非空的石子
- 解:當且僅當下述命題成立,先手必勝
- 存在t使得
sum(ai & (1<<t))%(k+1)!=0
代數結構
置換群
- 求 \(A^x\),編號從 $0$ 開始,\(O(n)\)
void qpow(int a[],int n,int x){
static int rec[N],c[N];
static bool vis[N];
fill(vis,vis+n,0);
repeat(i,0,n)if(!vis[i]){
int cnt=0; rec[cnt++]=i;
for(int p=a[i];p!=i;p=a[p])
rec[cnt++]=p,vis[p]=1;
repeat(J,0,cnt)
c[rec[J]]=a[rec[(J+x-1)%cnt]];
repeat(J,0,cnt)
a[rec[J]]=c[rec[J]];
}
}
- \(A^k=B\) 求任一 \(A\),編號從 $0$ 開始,\(O(n)\)(暫無判斷有解操作)
repeat(i,0,n){
a[read()-1]=i;
vis[i]=0;
}
repeat(i,0,n)if(!vis[i]){
int cnt=0; rec[cnt++]=i;
for(int p=a[i];p!=i;p=a[p])
rec[cnt++]=p,vis[p]=1;
repeat(J,0,cnt)
c[1ll*J*k%cnt]=rec[J];
repeat(J,0,cnt)
ans[c[(J+1)%cnt]]=c[J];
}
多項式
拉格朗日插值
- 函數曲線通過n個點 \((x_i,y_i)\),求 \(f(k)\)
- 拉格朗日插值:\(f(x)=\sum\limits_{i=1}^n[y_i\Pi_{j!=i}\dfrac{x-x_j}{x_i-x_j}]\)
- \(O(n^2)\)
repeat(i,0,n)x[i]%=mod,y[i]%=mod;
repeat(i,0,n){
s1=y[i];
s2=1;
repeat(j,0,n)
if(i!=j){
s1=s1*(k-x[j])%mod;
s2=s2*(x[i]-x[j])%mod;
}
ans=(ans+s1*getinv(s2)%mod+mod)%mod;
}
快速傅里葉變換+任意模數
- 求兩個多項式的卷積,\(O(n\log n)\)
struct FFT{
static const int N=1<<20;
struct cp{
long double a,b;
cp(){}
cp(const long double &a,const long double &b):a(a),b(b){}
cp operator+(const cp &t)const{return cp(a+t.a,b+t.b);}
cp operator-(const cp &t)const{return cp(a-t.a,b-t.b);}
cp operator*(const cp &t)const{return cp(a*t.a-b*t.b,a*t.b+b*t.a);}
cp conj()const{return cp(a,-b);}
};
cp wn(int n,int f){
static const long double pi=acos(-1.0);
return cp(cos(pi/n),f*sin(pi/n));
}
int g[N];
void dft(cp a[],int n,int f){
repeat(i,0,n)if(i>g[i])swap(a[i],a[g[i]]);
for(int i=1;i<n;i<<=1){
cp w=wn(i,f);
for(int j=0;j<n;j+=i<<1){
cp e(1,0);
for(int k=0;k<i;e=e*w,k++){
cp x=a[j+k],y=a[j+k+i]*e;
a[j+k]=x+y,a[j+k+i]=x-y;
}
}
}
if(f==-1){
cp Inv(1.0/n,0);
repeat(i,0,n)a[i]=a[i]*Inv;
}
}
#ifdef CONV
cp a[N],b[N];
vector<ll> conv(const vector<ll> &u,const vector<ll> &v){ //一般fft
const int n=(int)u.size()-1,m=(int)v.size()-1;
const int k=32-__builtin_clz(n+m+1),s=1<<k;
g[0]=0; repeat(i,1,s)g[i]=(g[i/2]/2)|((i&1)<<(k-1));
repeat(i,0,s){
a[i]=cp(i<=n?u[i]:0,0);
b[i]=cp(i<=m?v[i]:0,0);
}
dft(a,s,1); dft(b,s,1);
repeat(i,0,s)a[i]=a[i]*b[i];
dft(a,s,-1);
vector<ll> ans;
repeat(i,0,n+m+1)ans+=llround(a[i].a);
return ans;
}
#endif
#ifdef CONV_MOD
cp a[N],b[N],Aa[N],Ab[N],Ba[N],Bb[N];
vector<ll> conv_mod(const vector<ll> &u,const vector<ll> &v,ll mod){ //任意模數fft
const int n=(int)u.size()-1,m=(int)v.size()-1,M=sqrt(mod)+1;
const int k=32-__builtin_clz(n+m+1),s=1<<k;
g[0]=0; repeat(i,1,s)g[i]=(g[i/2]/2)|((i&1)<<(k-1));
repeat(i,0,s){
a[i]=i<=n?cp(u[i]%mod%M,u[i]%mod/M):cp();
b[i]=i<=m?cp(v[i]%mod%M,v[i]%mod/M):cp();
}
dft(a,s,1); dft(b,s,1);
repeat(i,0,s){
int j=(s-i)%s;
cp t1=(a[i]+a[j].conj())*cp(0.5,0);
cp t2=(a[i]-a[j].conj())*cp(0,-0.5);
cp t3=(b[i]+b[j].conj())*cp(0.5,0);
cp t4=(b[i]-b[j].conj())*cp(0,-0.5);
Aa[i]=t1*t3,Ab[i]=t1*t4,Ba[i]=t2*t3,Bb[i]=t2*t4;
}
repeat(i,0,s){
a[i]=Aa[i]+Ab[i]*cp(0,1);
b[i]=Ba[i]+Bb[i]*cp(0,1);
}
dft(a,s,-1); dft(b,s,-1);
vector<ll> ans;
repeat(i,0,n+m+1){
ll t1=llround(a[i].a)%mod;
ll t2=llround(a[i].b)%mod;
ll t3=llround(b[i].a)%mod;
ll t4=llround(b[i].b)%mod;
ans+=(t1+(t2+t3)*M%mod+t4*M*M)%mod;
}
return ans;
}
#endif
}fft;
多項式的一些概念
- 生成函數:\(A(x)=a_0+a_1x+a_2x^2+...\)
- 組合對象:x
- 組合對象的大小:x的指數i
- 方案數:系數
- 有 $1+x+x^2+...=\dfrac{1}{1-x}$
- 指數生成函數:無序排列
- 有 $1+x+\dfrac{x2}{2!}+\dfrac{x3}{3!}+...=e^x$
- 嚴重空缺
矩陣
矩陣乘法 矩陣快速冪
矩乘 \(O(n^3)\),矩快 \(O(n^3\log b)\)
struct mat{
static const int N=110;
ll a[N][N];
explicit mat(ll e=0){
repeat(i,0,n)
repeat(j,0,n)
a[i][j]=e*(i==j);
}
mat operator*(const mat &b)const{ //矩陣乘法
mat ans(0);
repeat(i,0,n)
repeat(j,0,n){
ll &t=ans.a[i][j];
repeat(k,0,n)
t=(t+a[i][k]*b.a[k][j])%mod;
}
return ans;
}
ll *operator[](int x){return a[x];}
const ll *operator[](int x)const{return a[x];}
};
mat qpow(mat a,ll b){ //矩陣快速冪
mat ans(1); //mat ans; repeat(i,0,n)ans[i][i]=1;
while(b){
if(b&1)ans=ans*a;
a=a*a; b>>=1;
}
return ans;
}
矩陣高級操作
- 行列式、逆矩陣(luogu P3389 && luogu P4783)
- \(O(n^3)\)
int n,m;
#define T ll
struct mat{
static const int N=110;
vector< vector<T> > a;
mat():a(N,vector<T>(N*2)){} //如果要求逆這里乘2
T det;
void r_div(int x,T k){ //第x行除以實數k
T r=getinv(k,mod);
repeat(i,0,m) //從x開始也沒太大關系(對求det來說)
a[x][i]=a[x][i]*r%mod;
det=det*k%mod;
}
void r_plus(int x,int y,T k){ //第x行加上第y行的k倍
repeat(i,0,m)
a[x][i]=(a[x][i]+a[y][i]*k)%mod;
}
/*
void r_div(int x,T k){ //lf版
T r=1/k;
repeat(i,0,m)a[x][i]*=r;
det*=k;
}
void r_plus(int x,int y,T k){ //lf版
repeat(i,0,m)a[x][i]+=a[y][i]*k;
}
*/
bool gauss(){ //高斯消元,返回是否滿秩,注意必須n<=m
det=1;
repeat(i,0,n){
int t=-1;
repeat(j,i,n)
if(abs(a[j][i])>eps){t=j; break;}
if(t==-1){det=0; return 0;}
if(t!=i){a[i].swap(a[t]); det=-det;}
r_div(i,a[i][i]);
repeat(j,0,n) //如果只要det可以從i+1開始
if(j!=i && abs(a[j][i])>eps)
r_plus(j,i,-a[j][i]);
}
return 1;
}
T get_det(){gauss(); return det;} //返回行列式
bool get_inv(){ //把自己變成逆矩陣,返回是否成功
if(n!=m)return 0;
repeat(i,0,n)
repeat(j,0,n)
a[i][j+n]=i==j; //生成增廣矩陣
m*=2; bool t=gauss(); m/=2;
repeat(i,0,n)
repeat(j,0,n)
a[i][j]=a[i][j+n];
return t;
}
//vector<T> &operator[](int x){return a[x];}
//const vector<T> &operator[](int x)const{return a[x];}
}a;
- 任意模數行列式(HDOJ 2827)
- \(O(n^3\log C)\)
int n;
struct mat{
static const int N=110;
vector< vector<ll> > a;
mat():a(N,vector<ll>(N)){}
ll det(int n){
ll ans=1;
repeat(i,0,n){
repeat(j,i+1,n)
while(a[j][i]){
ll t=a[i][i]/a[j][i];
repeat(k,i,n)a[i][k]=(a[i][k]-a[j][k]*t)%mod;
swap(a[i],a[j]);
ans=-ans;
}
ans=ans*a[i][i]%mod;
if(!ans)return 0;
}
return (ans+mod)%mod;
}
}a;
矩陣的一些結論
- \(n\times n\) 方陣 \(A\) 有:\(\left[\begin{array}{c}A&E\\O&E\end{array}\right]^{k+1}=\left[\begin{array}{c}A^k&E+A+A^2+...+A^k\\O&E\end{array}\right]\)
- 線性遞推轉矩快
- \(f_{n+3}=af_{n+2}+bf_{n+1}+cf_{n}\\\Leftrightarrow\left[\begin{array}{c}a&b&c\\1&0&0\\0&1&0\end{array}\right]^n \left[\begin{array}{c}f_2\\f_1\\f_0\end{array}\right]=\left[\begin{array}{c}f_{n+2}\\f_{n+1}\\f_{n}\end{array}\right]\)
線性基
- 線性基是一系列線性無關的基向量組成的集合
異或線性基
- 插入、查詢 \(O(\log M)\)
struct basis{
static const int n=63;
#define B(x,i) ((x>>i)&1)
ll a[n],sz;
bool failpush; //是否線性相關
void init(){mst(a,0); sz=failpush=0;}
void push(ll x){ //插入元素
repeat(i,0,n)if(B(x,i))x^=a[i];
if(x!=0){
int p=63-__builtin_clzll(x); sz++;
repeat(i,p+1,n)if(B(a[i],p))a[i]^=x;
a[p]=x;
}
else failpush=1;
}
ll top(){ //最大值
ll ans=0;
repeat(i,0,n)ans^=a[i];
return ans;
}
bool exist(ll x){ //是否存在
repeat_back(i,0,n)
if((x>>i)&1){
if(a[i]==0)return 0;
else x^=a[i];
}
return 1;
}
ll kth(ll k){ //第k小,不存在返回-1
if(failpush)k--; //如果認為0是可能的答案就加這句話
if(k>=(1ll<<sz))return -1;
ll ans=0;
repeat(i,0,n)
if(a[i]!=0){
if(k&1)ans^=a[i];
k>>=1;
}
return ans;
}
}b;
basis operator+(basis a,const basis &b){ //將b並入a
repeat(i,0,a.n)
if(b.a[i])a.push(b.a[i]);
a.failpush|=b.failpush;
return a;
}
- 這個版本中求kth需要rebuild \(O(\log^2 n)\)
struct basis{
//...
void push(ll x){ //插入元素
repeat_back(i,0,n)
if((x>>i)&1){
if(a[i]==0){a[i]=x; sz++; return;}
else x^=a[i];
}
failpush=1;
}
ll top(){ //最大值
ll ans=0;
repeat_back(i,0,n)
ans=max(ans,ans^a[i]);
return ans;
}
void rebuild(){ //求第k小的前置操作
repeat_back(i,0,n)
repeat_back(j,0,i)
if((a[i]>>j)&1)
a[i]^=a[j];
}
}b;
實數線性基
- 編號從 $0$ 開始,插入、查詢 \(O(n^2)\)
struct basis{
lf a[N][N]; bool f[N]; int n; //f[i]表示向量a[i]是否被占
void init(int _n){
n=_n;
fill(f,f+n,0);
}
bool push(lf x[]){ //返回0表示可以被線性表示,不需要插入
repeat(i,0,n)
if(abs(x[i])>1e-5){ //這個值要大一些
if(f[i]){
lf t=x[i]/a[i][i];
repeat(j,0,n)x[j]-=t*a[i][j];
}
else{
f[i]=1;
repeat(j,0,n)a[i][j]=x[j];
return 1;
}
}
return 0;
}
}b;
線性規划 | 單純形法
- 聲明:還沒學會
- \(\left[\begin{array}{ccccccc} a & a & a & a & a & a & b \\ a & a & a & a & a & a & b \\ a & a & a & a & a & a & b \\ c & c & c & c & c & c & v \end{array}\right]\)
- 每行表示一個約束,\(\sum ax\le b\),並且所有 \(x\ge 0\),求 \(\sum cx\) 的最大值
- 對偶問題:每列表示一個約束,\(\sum ax\ge c\),並且所有 \(x\ge 0\),求 \(\sum bx\) 的最小值
- 先找 \(c[y]>0\) 的 \(y\),再找 \(b[x]>0\) 且 \(\dfrac {b[x]}{a[x][y]}\) 最小的x(找不到 \(y\) 則 \(v\),找不到 \(x\) 則 INF),用行變換將 \(a[x][y]\) 置 $1$,將其他 \(a[i][y]\) 和 \(c[y]\) 置 $0$
- 編號從 $1$ 開始,\(O(n^3)\),缺init
const int M=1010; const lf eps=1e-6;
int n,m;
lf a[N][M],b[N],c[M],v; //a[1..n][1..m],b[1..n],c[1..m]
void pivot(int x,int y){
b[x]/=a[x][y];
repeat(j,1,m+1)if(j!=y)
a[x][j]/=a[x][y];
a[x][y]=1/a[x][y];
repeat(i,1,n+1)
if(i!=x && abs(a[i][y])>eps){
b[i]-=a[i][y]*b[x];
repeat(j,1,m+1)if(j!=y)
a[i][j]-=a[i][y]*a[x][j];
a[i][y]=-a[i][y]*a[x][y];
}
v+=c[y]*b[x];
repeat(j,1,m+1)if(j!=y)
c[j]-=c[y]*a[x][j];
c[y]=-c[y]*a[x][y];
}
lf simplex(){ //返回INF表示無限制,否則返回答案
while(1){
int x,y;
for(y=1;y<=m;y++)if(c[y]>eps)break;
if(y==m+1)return v;
lf mn=INF;
repeat(i,1,n+1)
if(a[i][y]>eps && mn>b[i]/a[i][y])
mn=b[i]/a[i][y],x=i;
if(mn==INF)return INF; //unbounded
pivot(x,y);
}
}
void init(){v=0;}
數學雜項
主定理
- 對於 \(T(n)=aT(\dfrac nb)+n^k\) (要估算 \(n^k\) 的 \(k\) 值)
- 若 \(\log_ba>k\),則 \(T(n)=O(n^{\log_ba})\)
- 若 \(\log_ba=k\),則 \(T(n)=O(n^k\log n)\)
- 若 \(\log_ba<k\)(有省略),則 \(T(n)=O(n^k)\)
質數表
42737, 46411, 50101, 52627, 54577, 191677, 194869, 210407, 221831, 241337, 578603, 625409, 713569, 788813, 862481, 2174729, 2326673, 2688877, 2779417, 3133583, 4489747, 6697841, 6791471, 6878533, 7883129, 9124553, 10415371, 11134633, 12214801, 15589333, 17148757, 17997457, 20278487, 27256133, 28678757, 38206199, 41337119, 47422547, 48543479, 52834961, 76993291, 85852231, 95217823, 108755593, 132972461, 171863609, 173629837, 176939899, 207808351, 227218703, 306112619, 311809637, 322711981, 330806107, 345593317, 345887293, 362838523, 373523729, 394207349, 409580177, 437359931, 483577261, 490845269, 512059357, 534387017, 698987533, 764016151, 906097321, 914067307, 954169327
1572869, 3145739, 6291469, 12582917, 25165843, 50331653 (適合哈希的素數)
19260817 原根15,是某個很好用的質數 1000000007 原根5 998244353 原根3
- NTT素數表, \(g\) 是模 \((r \cdot 2^k+1)\) 的原根
r*2^k+1 r k g
3 1 1 2
5 1 2 2
17 1 4 3
97 3 5 5
193 3 6 5
257 1 8 3
7681 15 9 17
12289 3 12 11
40961 5 13 3
65537 1 16 3
786433 3 18 10
5767169 11 19 3
7340033 7 20 3
23068673 11 21 3
104857601 25 22 3
167772161 5 25 3
469762049 7 26 3
998244353 119 23 3
1004535809 479 21 3
2013265921 15 27 31
2281701377 17 27 3
3221225473 3 30 5
75161927681 35 31 3
77309411329 9 33 7
206158430209 3 36 22
2061584302081 15 37 7
2748779069441 5 39 3
6597069766657 3 41 5
39582418599937 9 42 5
79164837199873 9 43 5
263882790666241 15 44 7
1231453023109121 35 45 3
1337006139375617 19 46 3
3799912185593857 27 47 5
4222124650659841 15 48 19
7881299347898369 7 50 6
31525197391593473 7 52 3
180143985094819841 5 55 6
1945555039024054273 27 56 5
4179340454199820289 29 57 3
struct of 自動取模
- 不好用,別用了
struct mint{
ll v;
mint(ll _v){v=_v%mod;}
mint operator+(const mint &b)const{return v+b.v;}
mint operator-(const mint &b)const{return v-b.v;}
mint operator*(const mint &b)const{return v*b.v;}
explicit operator ll(){return (v+mod)%mod;}
};
struct of 高精度
- 加、減、乘、單精度取模、小於號和等於號(其他不等號用rel_ops命名空間)
- 如果涉及除法,
那就完蛋,用java吧;如果不想打這么多行也用java吧(一定要讓隊友會寫java)
struct big{
vector<ll> a;
static const ll k=1000000000,w=9;
int size()const{return a.size();}
explicit big(const ll &x=0){ //接收ll
*this=big(to_string(x));
}
explicit big(const string &s){ //接收string
static ll p10[9]={1};
repeat(i,1,w)p10[i]=p10[i-1]*10;
int len=s.size();
int f=(s[0]=='-')?-1:1;
a.resize(len/w+1);
repeat(i,0,len-(f==-1))
a[i/w]+=f*(s[len-1-i]-48)*p10[i%w];
adjust();
}
int sgn(){return a.back()>=0?1:-1;} //這個只能在強/弱調整后使用
void shrink(){ //收縮(內存不收縮)
while(size()>1 && a.back()==0)a.pop_back();
}
void adjust(){ //弱調整
repeat(i,0,3)a.push_back(0);
repeat(i,0,size()-1){
a[i+1]+=a[i]/k;
a[i]%=k;
}
shrink();
}
void final_adjust(){ //強調整
adjust();
int f=sgn();
repeat(i,0,size()-1){
ll t=(a[i]+k*f)%k;
a[i+1]+=(a[i]-t)/k;
a[i]=t;
}
shrink();
}
explicit operator string(){ //轉換成string
static char s[N]; char *p=s;
final_adjust();
if(sgn()==-1)*p++='-';
repeat_back(i,0,size())
sprintf(p,i==size()-1?"%lld":"%09lld",abs(a[i])),p+=strlen(p);
return s;
}
const ll &operator[](int n)const{ //訪問
return a[n];
}
ll &operator[](int n){ //彈性訪問
repeat(i,0,n-size()+1)a.push_back(0);
return a[n];
}
};
big operator+(big a,const big &b){
repeat(i,0,b.size())a[i]+=b[i];
a.adjust();
return a;
}
big operator-(big a,const big &b){
repeat(i,0,b.size())a[i]-=b[i];
a.adjust();
return a;
}
big operator*(const big &a,const big &b){
big ans;
repeat(i,0,a.size()){
repeat(j,0,b.size())
ans[i+j]+=a[i]*b[j];
ans.adjust();
}
return ans;
}
void operator*=(big &a,ll b){ //有時被卡常
big ans;
repeat(i,0,a.size())a[i]*=b;
a.adjust();
}
ll operator%(const big &a,ll mod){
ll ans=0,p=1;
repeat(i,0,a.size()){
ans=(ans+p*a[i])%mod;
p=(p*a.k)%mod;
}
return (ans+mod)%mod;
}
bool operator<(big a,big b){
a.final_adjust();
b.final_adjust();
repeat_back(i,0,max(a.size(),b.size()))
if(a[i]!=b[i])return a[i]<b[i];
return 0;
}
bool operator==(big a,big b){
a.final_adjust();
b.final_adjust();
repeat_back(i,0,max(a.size(),b.size()))
if(a[i]!=b[i])return 0;
return 1;
}
表達式求值
inline int lvl(const string &c){ //運算優先級,小括號要排最后
if(c=="*")return 2;
if(c=="(" || c==")")return 0;
return 1;
}
string convert(const string &in) { //中綴轉后綴
stringstream ss;
stack<string> op;
string ans,s;
repeat(i,0,in.size()-1){
ss<<in[i];
if(!isdigit(in[i]) || !isdigit(in[i+1])) //插入空格
ss<<" ";
}
ss<<in.back();
while(ss>>s){
if(isdigit(s[0]))ans+=s+" ";
else if(s=="(")op.push(s);
else if(s==")"){
while(!op.empty() && op.top()!="(")
ans+=op.top()+" ",op.pop();
op.pop();
}
else{
while(!op.empty() && lvl(op.top())>=lvl(s))
ans+=op.top()+" ",op.pop();
op.push(s);
}
}
while(!op.empty())ans+=op.top()+" ",op.pop();
return ans;
}
ll calc(const string &in){ //后綴求值
stack<ll> num;
stringstream ss;
ss<<in;
string s;
while(ss>>s){
char c=s[0];
if(isdigit(c))
num.push((stoll(s))%mod);
else{
ll b=num.top(); num.pop();
ll a=num.top(); num.pop();
if(c=='+')num.push((a+b)%mod);
if(c=='-')num.push((a-b)%mod);
if(c=='*')num.push((a*b)%mod);
//if(c=='^')num.push(qpow(a,b));
}
}
return num.top();
}
一些數學結論
約瑟夫問題
- n個人編號0..(n-1),每次數到k出局,求最后剩下的人的編號
- 線性算法,\(O(n)\)
int jos(int n,int k){
int res=0;
repeat(i,1,n+1)res=(res+k)%i;
return res; //res+1,如果編號從1開始
}
- 對數算法,適用於k較小情況,\(O(k\log n)\)
int jos(int n,int k){
if(n==1 || k==1)return n-1;
if(k>n)return (jos(n-1,k)+k)%n; //線性算法
int res=jos(n-n/k,k)-n%k;
if(res<0)res+=n; //mod n
else res+=res/(k-1); //還原位置
return res; //res+1,如果編號從1開始
}
格雷碼 漢諾塔
格雷碼
- 一些性質:
- 相鄰格雷碼只變化一次
grey(n-1)
到grey(n)
修改了二進制的第(__builtin_ctzll(n)+1)
位grey(0)..grey(2^k-1)
是k維超立方體頂點的哈密頓回路,其中格雷碼每一位代表一個維度的坐標- 格雷碼變換,正 \(O(1)\),逆 \(O(logn)\)
ll grey(ll n){ //第n個格雷碼
return n^(n>>1);
}
ll degrey(ll n){ //逆格雷碼變換,法一
repeat(i,0,63) //or 31
n=n^(n>>1);
return n;
}
ll degrey(ll n){ //逆格雷碼變換,法二
int ans=0;
while(n){
ans^=n;
n>>=1;
}
return ans;
}
漢諾塔
- 假設盤數為n,總共需要移動
(1<<n)-1
次 - 第k次移動第
i=__builtin_ctzll(n)+1
小的盤子 - 該盤是第
(k>>i)+1
次移動 - (可以算出其他盤的狀態:總共移動了
((k+(1<<(i-1)))>>i)
次) - 該盤的移動順序是:
A->C->B->A(當i和n奇偶性相同)
A->B->C->A(當i和n奇偶性不同)
cin>>n; //層數
repeat(k,1,(1<<n)){
int i=__builtin_ctzll(k)+1;
int p1=(k>>i)%3; //移動前狀態
int p2=(p1+1)%3; //移動后狀態
if(i%2==n%2){
p1=(3-p1)%3;
p2=(3-p2)%3;
}
cout<<"move "<<i<<": "<<"ABC"[p1]<<" -> "<<"ABC"[p2]<<endl;
}
- 4個柱子的漢諾塔情況:令 \(k=\lfloor n+1-\sqrt{2n+1}+0.5\rfloor\),讓前k小的盤子用4個柱子的方法移到2號柱,其他盤子用3個柱子的方法移到4號柱,最后再移一次前k小,最短步數 \(f(n)=2f(k)+2^{n-k}-1\)
Stern-Brocot樹 Farey序列
- 分數序列:在 \([\dfrac 0 1,\dfrac 1 0]\) 中不斷在 \(\dfrac a b\) 和 \(\dfrac c d\) 之間插入 \(\dfrac {a+c}{b+d}\)
- 性質:所有數都是既約分數、可遍歷所有既約分數、保持單調遞增
- Stern-Brocot樹:二叉樹,其第 \(k\) 行是分數序列第 \(k\) 次操作新加的數
- Farey序列:\(F_n\) 是所有分子分母 \(\le n\) 的既約分數按照分數序列順序排列后的序列
- \(F_n\) 的長度 \(=1+\sum\limits_{i=1}^n\varphi(i)\)
浮點與近似計算
數值積分 | 自適應辛普森法
- 求 \(\int_{l}^{r}f(x)\mathrm{d}x\) 的近似值
lf raw(lf l,lf r){ //辛普森公式
return (f(l)+f(r)+4*f((l+r)/2))*(r-l)/6;
}
lf asr(lf l,lf r,lf eps,lf ans){
lf m=(l+r)/2;
lf x=raw(l,m),y=raw(m,r);
if(abs(x+y-ans)<=15*eps)
return x+y-(x+y-ans)/15;
return asr(l,m,eps/2,x)+asr(m,r,eps/2,y);
}
//調用方法:asr(l,r,eps,raw(l,r))
牛頓迭代法
- 求 \(f(x)\) 的零點:\(x_{n+1}=x_n-\dfrac{f(x)}{f'(x)}\)
- 檢驗 \(x_{n+1}=g(x_n)\) 多次迭代可以收斂於 \(x_0\) 的方法:看 \(|g'(x_0)|\le1\) 是否成立
lf newton(lf n){ //sqrt
lf x=1;
while(1){
lf y=(x+n/x)/2;
if(abs(x-y)<eps)return x;
x=y;
}
}
- java高精度的整數平方根
public static BigInteger isqrtNewton(BigInteger n){
BigInteger a=BigInteger.ONE.shiftLeft(n.bitLength()/2);
boolean d=false;
while(true){
BigInteger b=n.divide(a).add(a).shiftRight(1);
if(a.compareTo(b)==0 || a.compareTo(b)<0 && d)
break;
d=a.compareTo(b)>0;
a=b;
}
return a;
}
others of 浮點與近似計算
- \(\lim\limits_{n\rightarrow\infty}\dfrac{錯排(n)}{n!}=\dfrac 1 e,e\approx 2.718281828459045235360287471352\)
- \(\lim\limits_{n\rightarrow\infty}(\sum\frac 1 n-\ln n)=\gamma\approx 0.577215664901532860606\)
others of 數學雜項
- 埃及分數Engel展開
- 待展開的數為 \(x\),令 \(u_1=x, u_{i+1}=u_i\times\lceil\dfrac 1 {u_i}\rceil-1\)(到0為止)
- 令 \(a_i=\lceil\dfrac 1 {u_i}\rceil\)
- 則 \(x=\dfrac 1{a_1}+\dfrac 1{a_1a_2}+\dfrac 1{a_1a_2a_3}+...\)
- 三個水杯容量為 \(a,b,c\)(正整數),\(a=b+c\),初始 \(a\) 裝滿水,則得到容積為 \(\dfrac a 2\) 的水需要倒 \(\dfrac a{\gcd(b,c)}-1\) 次水
- 蘭頓螞蟻(白色異或右轉,黑色異或左轉),約一萬步后出現周期為104步的無限重復(高速公路)
- 任意勾股數能由復數 \((a+bi)^2\space(a,b∈\Z)\) 得到
- 任意正整數 \(a\) 都存在正整數 \(b,c\) 使得 \(a<b<c\) 且 \(a^2,b^2,c^2\) 成等差數列:構造 \(b=5a,c=7a\)
- 拉格朗日四平方和定理:每個正整數都能表示為4個整數平方和
- 對於偶素數 $2$ 有 $2=12+12+02+02$
- 對於奇素數 \(p\) 有 \(p=a^2+b^2+1^2+0^2\) (容斥可證)
- 對於所有合數 \(n\) 有 \(n=z_1^2+z_2^2+z_3^2+z_4^2=(x_1^2+x_2^2+x_3^2+x_4^2)\cdot(y_1^2+y_2^2+y_3^2+y_4^2)\)
- 其中 \(\begin{cases} z_1=x_1y_1+x_2y_2+x_3y_3+x_4y_4 \\ z_2=x_1y_2-x_2y_1-x_3y_4+x_4y_3 \\ z_3=x_1y_3-x_3y_1+x_2y_4-x_4y_2 \\ z_4=x_1y_4-x_4y_1-x_2y_3+x_3y_2\end{cases}\)
- 基姆拉爾森公式
- 已知年月日,返回星期幾
int week(int y,int m,int d){
if(m<=2)m+=12,y--;
return (d+2*m+3*(m+1)/5+y+y/4-y/100+y/400)%7+1;
}