Loading [MathJax]/extensions/TeX/newcommand.js
\newcommand{\ord}[1]{\mathcal{O}\left(#1\right)} \newcommand{\abs}[1]{\lvert #1 \rvert} \newcommand{\floor}[1]{\lfloor #1 \rfloor} \newcommand{\ceil}[1]{\lceil #1 \rceil} \newcommand{\opord}{\operatorname{\mathcal{O}}} \newcommand{\argmax}{\operatorname{arg\,max}} \newcommand{\str}[1]{\texttt{"#1"}}

2016年12月7日 星期三

[ 0-1 Fractional Programming Dinkelbach algorithm ] 0-1分數規劃

【定義】

給你兩個數組,benefit[i]表示選i的利潤、cost[i]表示選i的花費。
如果選i,定義x[i]=1否則x[i]=0,通常題目會給你選擇的限制條件,像是必須是一顆生成樹之類的,這裡就把它忽略掉
我們的目標是要求\frac{\sum{benefit[i] \times x[i]}}{\sum{cost[i] \times x[i]}}的最大值。

【分析】

先不管最大化問題,令\frac{\sum{benefit[i] \times x[i]}}{\sum{cost[i] \times x[i]}}=L
可以把式子改寫成(\sum{benefit[i] \times x[i]})-L \times (\sum{cost[i] \times x[i]})=0
分離參數得到\sum{(benefit[i]-L \times  cost[i]) \times x[i]}=0
只要知道L(benefit[i]-L \times  cost[i])是直接可以求出來的,先記他為d(i,L)
那可以設f(L)=\sum{d(i,L) \times x[i]}

我們的目標變成是要最大化L
那若存在一種x的選法使得f(L)>0能夠證明什麼呢?
f(L)>0 \to \frac{\sum{benefit[i] \times x[i]}}{\sum{cost[i] \times x[i]}}>L那表示存在另一個L會比現在的L還要好
f(L)<0?很明顯這種情況完全沒有意義,因為找不到這種L

最佳的L會讓所有x的選法都f(L)\leq0,只要找到這樣的L就說明他是最佳解了
也可以用類似的想法求最小化L,這裡就不贅述了。

【解法】

根據剛剛找出來的性質f(L)是單調性的,我們可以二分搜L,然後驗證是否存在一組解使得f(L)>0,這很簡單就不附code了。
另一個有效率的算法是Dinkelbach,他跟二分搜不一樣的地方是他要去求解,在驗證解跟求解的複雜度一樣的時候Dinkelbach是會比較快的,但有時候求解的複雜度會比驗證解的複雜度還要高,因此要看情況使用
以下附上虛擬碼:
Dinkelbach(){
L=任意狀態(通常設為0);
do{
Ans=L;
for(i:所有元素)d[i]=benefit[i]-L*cost[i];//計算d值
找出一組使f(L)最大的x;
p=0,q=0;
for(i:所有元素){
if(x[i])p+=benefit[i],q+=cost[i];
}
L=p/q;//更新解,注意q=0的情況
}while(abs(Ans-L)>EPS);
return Ans;
}
view raw Dinkelbach.cpp hosted with ❤ by GitHub

常見的題型有:最優比率環、最優比率生成樹、最大密度子圖(有flow解)等

2016年12月6日 星期二

[ dominator tree ] 有向圖的支配樹

dominator tree,中文名稱叫支配樹
給一張有向圖G,我們從其中一個點r出發到y的所有路徑中,一定會經過點x,就稱xy的必經點;我們把距離y最近的必經點稱為最近必經點,記為idom(y)
支配樹上的有向邊(u,v)滿足idom(v)=u,那對於一個點yy的所有必經點就會是支配樹上ry路徑上的所有點。
這個東西可以求有向圖的割點、橋(在每個邊加入虛擬點)等等。
注意code裡的idom跟我定義的不一樣
我是看這份講義的偽代碼寫出來的:

struct dominator_tree{
static const int MAXN=5005;
int n;// 1-base
vector<int> G[MAXN],rG[MAXN];//存圖和反向圖
int pa[MAXN],dfn[MAXN],id[MAXN],dfnCnt;
int semi[MAXN],idom[MAXN],best[MAXN];
vector<int> tree[MAXN];//dominator_tree存這裡
void init(int _n){
n=_n;
for(int i=1;i<=n;++i)G[i].clear(),rG[i].clear();
}
void add_edge(int u,int v){
G[u].push_back(v);
rG[v].push_back(u);
}
void dfs(int u){
id[dfn[u]=++dfnCnt]=u;
for(auto v:G[u]) if(!dfn[v]){
dfs(v),pa[dfn[v]]=dfn[u];
}
}
int find(int y,int x){
if(y<=x)return y;
int tmp=find(pa[y],x);
if(semi[best[y]]>semi[best[pa[y]]])
best[y]=best[pa[y]];
return pa[y]=tmp;
}
void tarjan(int root){
dfnCnt=0;
for(int i=1;i<=n;++i){
dfn[i]=idom[i]=0;
tree[i].clear();
best[i]=semi[i]=i;
}
dfs(root);
for(int i=dfnCnt;i>1;--i){
int u=id[i];
for(auto v:rG[u]) if(v=dfn[v]){
find(v,i);
semi[i]=min(semi[i],semi[best[v]]);
}
tree[semi[i]].push_back(i);
for(auto v:tree[pa[i]]){
find(v,pa[i]);
idom[v] = semi[best[v]]==pa[i] ? pa[i] : best[v];
}
tree[pa[i]].clear();
}
for(int i=2; i<=dfnCnt; ++i){
if(idom[i]!=semi[i]) idom[i]=idom[idom[i]];
tree[id[idom[i]]].push_back(id[i]);
}
}
};

2016年11月3日 星期四

[ Pi algorithms - John Machin's formula ] 梅欽公式計算圓周率

我們知道
\frac{\pi}{4}= \arctan 1
\arctan(x)的泰勒展開式長這樣
\arctan(x)=x-\frac{x^3}{3}+\frac{x^5}{5}-\frac{x^7}{7}+\frac{x^9}{9}- \cdots \; (x \leq 1)
故可以求出莱布尼茨公式如下:
\frac{\pi}{4}= 1 \,-\, \frac{1}{3} \,+\, \frac{1}{5} \,-\, \frac{1}{7} \,+\, \frac{1}{9} \,-\, \cdots
但是這用來計算\pi到小數點後指定位數是非常困難的,因此有了以下的梅欽公式:
\frac{\pi}{4} = 4 \arctan \frac{1}{5} - \arctan \frac{1}{239}
再用泰勒展開,可以得到:
\pi=(\frac{16}{5} - \frac{16} {3*5^3} + \frac{16}{5 * 5^5} - \frac{16} {7 * 5^7} + \cdots) \\ - (\frac{4}{239} - \frac{4}{3 * 239^3} + \frac{4}{5 * 239^5} - \frac{4}{7 * 239^7} + \cdots)
可以將這個公式整理為:
\pi=\frac{\frac{16}{5} - \frac{4}{239}}{1} -\frac{\frac{16}{5^3} - \frac{4}{239^3}}{3} + \frac{\frac{16}{5^5} - \frac{4}{239^5}}{5} - \cdots
如果我們要計算圓周率至10的負L次方,由於\frac{16}{5^{2*n-1}} - \frac{4}{239^{2*n-1}}\frac{16}{5^{2*n-1}}\frac{4}{239^{2*n-1}}來的大,具有決定性,所以表示至少必須計算至第n項:
\frac{16}{(2*n-1)*5^{2*n-1}}=10^{-L}
將上面的等式取log並經過化簡,我們可以求得(誤差什麼得先不管它):
n =\frac{L}{2log_{10} 5} = \frac{L}{1.39794}
這樣就可以計算\pi到小數點後第L位了

這裡有其他可以快速算出圓周率的梅欽類公式
http://jeux-et-mathematiques.davalan.org/calc/formulespi.pdf

2016年10月28日 星期五

[ Earley parser ] Earley語法解析器

有一天蚯蚓跟我說CYK算法很慢,推薦了這個東西,所以我就把他寫出來了。
他主要是先建構出類似於自動機的圖,有很多個狀態在跳來跳去,在我的實作裡有將其轉成語法樹的方法。
這裡需要自己實作的有lexer,還有最後的語法一定要有一個gamma rule,gamma rule就是有一個gamma \Longrightarrow SS是初始符號。
首先是語法規則的部分:
struct Rule{
vector<vector<Rule*> > p;
void add(const vector<Rule*> &l){
p.push_back(l);
}
};
map<string,Rule*> NameRule;
map<Rule*,string> RuleName;
inline void init_Rule(){
for(auto r:RuleName)delete r.first;
RuleName.clear();
NameRule.clear();
}
inline Rule *add_rule(const string &s){
if(NameRule.find(s)!=NameRule.end())return NameRule[s];
Rule *r=new Rule();
RuleName[r]=s;
NameRule[s]=r;
return r;
}
//範例
inline Rule *get_my_Rule(){
//可以由題目輸入語法,也可以自己設定
Rule *S=add_rule("S"),*E=add_rule("E"),*L=add_rule("L");
Rule *list=add_rule("("),*AND=add_rule(")"),*T=add_rule("T");
S->add({list,E});
S->add({list,L});
L->add({E,L});
L->add({E,AND,E});
E->add({T});
E->add({S});
Rule *GAMMA=add_rule("GAMMA");//一定要有gamma rule當作是最上層的語法
GAMMA->add({S});
return GAMMA;
}
view raw rule.cpp hosted with ❤ by GitHub

這是分析器的本體,lexer的部分要自己去完成,lexer的結果會是parse的第二個參數,計算出來一些資料會存在table裡面
以下為模板:
typedef vector<Rule*> production;
struct State{
Rule *r;
int rid,dot_id,start,end;
State(Rule *r,int rid,int dot,int start):r(r),rid(rid),dot_id(dot),start(start),end(-1){}
State(Rule *r=0,int col=0):r(r),rid(-1),dot_id(-1),start(-1),end(col){}
bool completed()const{
return rid==-1||dot_id>=(int)r->p[rid].size();
}
Rule *next_term()const{
if(completed())return 0;
return r->p[rid][dot_id];
}
bool operator<(const State& b)const{
if(start!=b.start)return start<b.start;
if(dot_id!=b.dot_id)return dot_id<b.dot_id;
if(r!=b.r)return r<b.r;
return rid<b.rid;
}
void print()const{
cout<<RuleName[r]<<"->";
if(rid!=-1)for(size_t i=0;;++i){
if((int)i==dot_id)cout<<" "<<"$";
if(i>=r->p[rid].size())break;
cout<<" "<<RuleName[r->p[rid][i]];
}
cout<<" "<<"["<<start<<","<<end<<"]"<<endl;
}
};
struct Column{
Rule *term;
string value;
vector<State> s;
map<State,set<pair<State,State> > > div;
//div比較像一棵 左兄右子的樹
Column(Rule *r,const string &s):term(r),value(s){}
Column(){}
bool add(const State &st,int col){
if(div.find(st)==div.end()){
div[st];
s.push_back(st);
s.back().end=col;
return true;
}else return false;
}
};
inline vector<Column> lexer(string text){
//tokenize,要自己寫
//他會把 input stream 變成 token stream,就是(terminal,value)pair
vector<Column> token;
//自己寫的地方
return token;
}
vector<Column> table;
inline void predict(int col,Rule *rul){
for(size_t i=0;i<rul->p.size();++i){
table[col].add(State(rul,i,0,col),col);
}
}
inline void scan(int col,const State &s,Rule *r){
if(r!=table[col].term)return;
State ns(s.r,s.rid,s.dot_id+1,s.start);
table[col].add(ns,col);
table[col].div[ns].insert(make_pair(s,State(r,col)));
}
inline void complete(int col,const State &s){
for(size_t i=0;i<table[s.start].s.size();++i){
State &st=table[s.start].s[i];
Rule *term=st.next_term();
if(!term||term->p.size()==0)continue;
if(term==s.r){
State nst(st.r,st.rid,st.dot_id+1,st.start);
table[col].add(nst,col);
table[col].div[nst].insert(make_pair(st,s));
}
}
}
inline pair<bool,State> parse(Rule *GAMMA,const vector<Column > &token){
table.resize(token.size()+1);
for(size_t i=0;i<token.size();++i){
table[i+1]=Column(token[i]);
}
table[0]=Column();
table[0].add(State(GAMMA,0,0,0),0);
for(size_t i=0;i<table.size();++i){
for(size_t j=0;j<table[i].s.size();++j){
State state=table[i].s[j];
if(state.completed())complete(i,state);
else{
Rule *term=state.next_term();
if(term->p.size())predict(i,term);
else if(i+1<table.size())scan(i+1,state,term);
}
}
}
for(size_t i=0;i<table.back().s.size();++i){
if(table.back().s[i].r==GAMMA&&table.back().s[i].completed()){
return make_pair(true,table.back().s[i]);
}
}
return make_pair(false,State(0,-1));
}
view raw parser.cpp hosted with ❤ by GitHub

最後是構造語法樹的部分,因為div是一顆左兄弟右兒子的樹,所以要將她轉換成正常的語法樹,還要去記錄曖昧文法的部分:
struct node{//語法樹的節點
State s;
vector<vector<node*> > child;//vector<node*>.size()>1表示ambiguous
node(const State &s):s(s){}
node(){}
};
struct State_end_cmp{
bool operator()(const State &a,const State &b)const{
return a.end<b.end||(a.end==b.end&&a<b);
}
};
map<State,node*,State_end_cmp> cache;
vector<node*> node_set;
inline void init_cache(){
for(auto d:node_set)delete d;
cache.clear();
node_set.clear();
}
void _build_tree(const State &s,node *pa,bool amb=0){
if(cache.find(s)!=cache.end()){
pa->child.push_back(vector<node*>(1,cache[s]));
return;
}
node *o;
if(s.completed()){
o=new node(s);
if(amb)pa->child.back().push_back(o);
else pa->child.push_back(vector<node*>(1,o));
}else o=pa->child.back().back();
amb=0;
for(auto div:table[s.end].div[s]){
if(!amb)_build_tree(div.first,pa);
_build_tree(div.second,o,amb);
amb=1;
}
if(s.completed())cache[s]=o;
}
inline node *build_tree(const State &s){
init_cache();
node o;
_build_tree(s,&o);
assert(o.child.size()==1);
assert(o.child.back().size()==1);
return o.child.back().back();
}
void print_tree(node *o,int dep=0){
cout<<string(dep,' '),o->s.print();
for(auto div:o->child){
for(auto nd:div){
print_tree(nd,dep+2);
}
}
}
view raw build_tree.cpp hosted with ❤ by GitHub

2016年10月11日 星期二

[ Cocke-Younger-Kasami algorithm ] CYK算法

它是一個用來判定任意給定的字符串是否屬於某一個上下文無關文法的算法,還可以順便構造出二元語法樹和判斷是否有曖昧文法(Ambiguous Grammar)。對於一個任意給定的上下文無關文法,都可以使用CYK算法來計算上述問題,所以它是一個幾乎萬能的算法,但首先要將該文法轉換成喬姆斯基範式(CNF)。
這個算法主要是利用動態規劃的思想,給一個有n個字符的字符串s,和R條語法規則,則他的計算複雜度為\ord{n^3R},空間複雜度為\ord{n^2R}。類似於optimal binary search tree的算法,對於每個s[l,r]我們去計算s[l,k]和s[k+1,r]能符合的所有規則。

我們用2016 NCPC的題目當例子
這裡給一個簡單的範例,給你一個語法規則如下 :
A \Longrightarrow AAAAAAA \qquad 20 \\ A \Longrightarrow AA \qquad 15 \\ A \Longrightarrow a \qquad 5
如果看不懂的,他大約的意思如下:
一個合法字串A可以是七個合法字串A所組成,也可以是兩個合法字串A所組成,也可以是小寫字母a所組成。
每個規則都給他一個權重,表示經過這個規則的轉換會有這些花費,我們會給一個目標字串,要找出把整個字串轉換成A的最小花費,例如給定字串aaaaaaaa,他的最小花費就是75。
這裡舉另一個例子:
A \Longrightarrow BA \qquad 10 \\ A \Longrightarrow bcd \qquad 5 \\ B \Longrightarrow c \qquad 4
給定字串ccbcd,他的最小花費就是33。
當然也會發生無法轉換的情況,這個時候叫要輸出NIR

首先必須要把規則轉成喬姆斯基範式,這裡我以數字當作規則的名稱,當y=-1時表示這個cnf是s->x的規則,否則是s->xy的規則。
#define MAXN 55
struct CNF{
int s,x,y;//s->xy | s->x, if y==-1
int cost;
CNF(){}
CNF(int s,int x,int y,int c):s(s),x(x),y(y),cost(c){}
};
int state;//規則數量
map<char,int> rule;//每個字元對應到的規則,小寫字母為終端字符
vector<CNF> cnf;
inline void init(){
state=0;
rule.clear();
cnf.clear();
}
inline void add_to_cnf(char s,const string &p,int cost){
if(rule.find(s)==rule.end())rule[s]=state++;
for(auto c:p)if(rule.find(c)==rule.end())rule[c]=state++;
if(p.size()==1){
cnf.push_back(CNF(rule[s],rule[p[0]],-1,cost));
}else{
int left=rule[s];
int sz=p.size();
for(int i=0;i<sz-2;++i){
cnf.push_back(CNF(left,rule[p[i]],state,0));
left=state++;
}
cnf.push_back(CNF(left,rule[p[sz-2]],rule[p[sz-1]],cost));
}
}
view raw make_cnf.cpp hosted with ❤ by GitHub
接著就可以跑我們的演算法啦:
vector<long long> dp[MAXN][MAXN];
vector<bool> neg_INF[MAXN][MAXN];//如果花費是負的可能會有無限小的情形
inline void relax(int l,int r,const CNF &c,long long cost,bool neg_c=0){
if(!neg_INF[l][r][c.s]&&(neg_INF[l][r][c.x]||cost<dp[l][r][c.s])){
if(neg_c||neg_INF[l][r][c.x]){
dp[l][r][c.s]=0;
neg_INF[l][r][c.s]=true;
}else dp[l][r][c.s]=cost;
}
}
inline void bellman(int l,int r,int n){
for(int k=1;k<=state;++k)
for(auto c:cnf)
if(c.y==-1)relax(l,r,c,dp[l][r][c.x]+c.cost,k==n);
}
inline void cyk(const vector<int> &tok){
for(int i=0;i<(int)tok.size();++i){
for(int j=0;j<(int)tok.size();++j){
dp[i][j]=vector<long long>(state+1,INT_MAX);
neg_INF[i][j]=vector<bool>(state+1,false);
}
dp[i][i][tok[i]]=0;
bellman(i,i,tok.size());
}
for(int r=1;r<(int)tok.size();++r){
for(int l=r-1;l>=0;--l){
for(int k=l;k<r;++k)
for(auto c:cnf)
if(~c.y)relax(l,r,c,dp[l][k][c.x]+dp[k+1][r][c.y]+c.cost);
bellman(l,r,tok.size());
}
}
}
view raw CYK.cpp hosted with ❤ by GitHub
當然它也可以用來判斷是不是有曖昧語法(ambiguous),只要做一些小修改就行了

2016年10月3日 星期一

[ cantor expansion ] 康托展开

給一個0 \sim n-1n個數字的全排列,求他是排名第幾的全排列;給一個名次,求全排列。這東西可以有效的減少hash_table的大小。
以下附上code:
#include<vector>
#define MAXN 11
int factorial[MAXN];
inline void init(){
factorial[0]=1;
for(int i=1;i<=MAXN;++i){
factorial[i]=factorial[i-1]*i;
}
}
inline int encode(const std::vector<int> &s){
int n=s.size(),res=0;
for(int i=0;i<n;++i){
int t=0;
for(int j=i+1;j<n;++j){
if(s[j]<s[i])++t;
}
res+=t*factorial[n-i-1];
}
return res;
}
inline std::vector<int> decode(int a,int n){
std::vector<int> res;
std::vector<bool> vis(n,0);
for(int i=n-1;i>=0;--i){
int t=a/factorial[i],j;
for(j=0;j<n;++j){
if(!vis[j]){
if(t==0)break;
--t;
}
}
res.push_back(j);
vis[j]=1;
a%=factorial[i];
}
return res;
}

使用前記得要呼叫init();

2016年10月1日 星期六

[ closest pair of points ] 平面上歐基里德距離最近點對

用分治法寫的,複雜度\ord{n\log^2 n}:
#include<cmath>
#include<vector>
#include<algorithm>
using namespace std;
template<typename T>
struct point{
T x,y;
point(){}
point(const T&dx,const T&dy):x(dx),y(dy){}
inline const point operator-(const point &b)const{
return point(x-b.x,y-b.y);
}
inline const T dot(const point &b)const{
return x*b.x+y*b.y;
}
inline const T abs2()const{/*向量長度的平方*/
return dot(*this);
}
static bool x_cmp(const point<T>& a,const point<T>& b){
return a.x<b.x;
}
static bool y_cmp(const point<T>& a,const point<T>& b){
return a.y<b.y;
}
};
#define INF LLONG_MAX/*預設是long long最大值*/
template<typename T>
T closest_pair(vector<point<T> >&v,vector<point<T> >&t,int l,int r){
T dis=INF,tmd;
if(l>=r)return dis;
int mid=(l+r)/2;
if((tmd=closest_pair(v,t,l,mid))<dis)dis=tmd;
if((tmd=closest_pair(v,t,mid+1,r))<dis)dis=tmd;
t.clear();
for(int i=l;i<=r;++i)
if((v[i].x-v[mid].x)*(v[i].x-v[mid].x)<dis)t.push_back(v[i]);
sort(t.begin(),t.end(),point<T>::y_cmp);/*如果用merge_sort的方式可以O(n)*/
for(int i=0;i<(int)t.size();++i)
for(int j=1;j<=3&&i+j<(int)t.size();++j)
if((tmd=(t[i]-t[i+j]).abs2())<dis)dis=tmd;
return dis;
}
template<typename T>
inline T closest_pair(vector<point<T> > &v){
vector<point<T> >t;
sort(v.begin(),v.end(),point<T>::x_cmp);
return closest_pair(v,t,0,v.size()-1);/*最近點對距離*/
}

2016年8月5日 星期五

[ tree centroid ] 樹重心

一棵無向樹T=(V,E),定義:
w_v(u)為點u的權重,u \in V
w_e(e)為邊e的權重,e \in E
d(u,v)uv路徑的權重和,u,v \in V

重心的定義是:
以這個點為根,那麼所有的子樹(不算整個樹自身)的大小都不超過整個樹大小的一半
即為最大的子樹最小的點

廣義的樹的重心:
無向樹T=(V,E)滿足
 w_v(u)≧0, \; \forall u \in V
 w_e(e)>0, \; \forall e \in E

c(u)=\sum_{v \in V}d(u,v)*w_v(v)
則樹重心 tree \; centroid=u \; | \; min(\{c(u):u \in V\})


 w_v(u)=1, \; \forall u \in V
 w_e(e)=1, \; \forall e \in E
的情況下,就是一般的樹重心

性質:
  1. 把兩個樹通過一條邊相連得到一個新的樹,那麼新的樹的重心在連接原來兩個樹的重心的路徑上。
  2. 把一個樹添加或刪除一個葉子,那麼它的重心最多只移動一條邊的距離。
用途:
樹分治、動態求樹重心等

可以利用DFS找出最大的子樹最小的點,即為樹重心
樹重心求法(用vector存無向樹):
#define MAXN 100005
#define INF 0x3f3f3f3f
int n;
vector<int> g[MAXN];
int size[MAXN];
int ans,balance_size;
inline void init(){
for(int i=0;i<n;++i)g[i].clear();
balance_size=INF;
}
void dfs(int u,int pa){
size[u]=1;
int max_son_size=0;
for(size_t i=0;i<g[u].size();++i){
int v=g[u][i];
if(v!=pa){
dfs(v,u);
size[u]+=size[v];
max_son_size=max(max_son_size,size[v]);
}
}
max_son_size=max(max_son_size,n-size[u]);
if(max_son_size<balance_size||/*找編號最小*/(max_son_size==balance_size&&u<ans)){
ans=u;
balance_size=max_son_size;
}
}
inline int tree_centroid(){
dfs(0,-1);
return ans;
}

[ bipartite graph multiple matching ] 二分圖多重匹配增廣路算法

這種問題的題目通常是這樣:

給一張圖G有n1個點和n2個點,n1個點之間沒有邊,n2個點之間也沒有邊,但是n1和n2個點之間有m條邊(簡單的來說就是n1個點和n2個點的二分圖啦),沒有重邊;其中n2個點每個點u都有一個可接受匹配數 c_u
n1的點只能跟一個點匹配,但n2的點在不超過可接受匹配數的情況下,可以跟多個點匹配,求這張圖的最大匹配

通常可以利用拆點的方法或是flow來做,但是這樣空間跟效率都不是很好,這裡提供一個有效率的方法:
#define MAXN1 1005
#define MAXN2 505
int n1,n2;//n1個點連向n2個點,其中n2個點可以匹配很多邊
vector<int > g[MAXN1];//圖
int c[MAXN2];//每個屬於n2點最多可以接受幾條匹配邊
vector<int> match_list[MAXN2];//每個屬於n2的點匹配了那些點
bool vis[MAXN2];//是否走訪過
bool dfs(int u){
for(size_t i=0;i<g[u].size();++i){
int v=g[u][i];
if(vis[v])continue;
vis[v]=true;
if((int)match_list[v].size()<c[v]){
match_list[v].push_back(u);
return true;
}else{
for(size_t j=0;j<match_list[v].size();++j){
int next_u=match_list[v][j];
if(dfs(next_u)){
match_list[v][j]=u;
return true;
}
}
}
}
return false;
}
inline int max_match(){
for(int i=0;i<n2;++i)match_list[i].clear();
int cnt=0;
for(int u=0;u<n1;++u){
memset(vis,0,sizeof(bool)*n2);
if(dfs(u))++cnt;
}
return cnt;
}

複雜度分析:
設n2個點每個點u都有一個值E_u表示和u相鄰的邊數,則總複雜度為
\ord{n1*(\sum{c_u*E_u}+n1+n2)}

2016年7月15日 星期五

[ Edmonds's general graph matching algorithm ] 一般圖最大匹配

關於匹配的教學我全部寫在這份投影片裡了:https://jacky860226.github.io/general-graph-weighted-match-slides/#/
所以這裡直接提供模板:
注意這裡必須要是無向圖
#define MAXN 505
vector<int>g[MAXN];//用vector存圖
int pa[MAXN],match[MAXN],st[MAXN],S[MAXN],vis[MAXN];
int t,n;
inline int lca(int u,int v){//找花的花托
for(++t;;swap(u,v)){
if(u==0)continue;
if(vis[u]==t)return u;
vis[u]=t;//這種方法可以不用清空vis陣列
u=st[pa[match[u]]];
}
}
#define qpush(u) q.push(u),S[u]=0
inline void flower(int u,int v,int l,queue<int> &q){
while(st[u]!=l){
pa[u]=v;//所有未匹配邊的pa都是雙向的
if(S[v=match[u]]==1)qpush(v);//所有奇點變偶點
st[u]=st[v]=l,u=pa[v];
}
}
inline bool bfs(int u){
for(int i=1;i<=n;++i)st[i]=i;//st[i]表示第i個點的集合
memset(S+1,-1,sizeof(int)*n);//-1:沒走過 0:偶點 1:奇點
queue<int>q;qpush(u);
while(q.size()){
u=q.front(),q.pop();
for(size_t i=0;i<g[u].size();++i){
int v=g[u][i];
if(S[v]==-1){
pa[v]=u,S[v]=1;
if(!match[v]){//有增廣路直接擴充
for(int lst;u;v=lst,u=pa[v])
lst=match[u],match[u]=v,match[v]=u;
return 1;
}
qpush(match[v]);
}else if(!S[v]&&st[v]!=st[u]){
int l=lca(st[v],st[u]);//遇到花,做花的處理
flower(v,u,l,q),flower(u,v,l,q);
}
}
}
return 0;
}
inline int blossom(){
memset(pa+1,0,sizeof(int)*n);
memset(match+1,0,sizeof(int)*n);
int ans=0;
for(int i=1;i<=n;++i)
if(!match[i]&&bfs(i))++ans;
return ans;
}
view raw blossom.cpp hosted with ❤ by GitHub

2016年5月19日 星期四

[ Kuhn-Munkres Algorithm ] 二分圖最大權完美匹配KM算法

關於此算法的介紹及教學請看這份投影片,這篇文章注重於討論\ord{N^4}的slack優化及\ord{N^4}\ord{N^3}的過程。

在網路上曾經看到這段話:

"實際上KM算法的複雜度是可以做到\ord{N^3}的。我們給每個y頂點一個“鬆弛量”函數slack,
每次開始找增廣路時初始化為無窮大。在尋找增廣路的過程中,檢查邊(i,j)時,如果它不在相等子圖中,則讓slack[j]變成原值與lx[i]+ly[j]-w[i,j]的較小值。這樣,在修改頂標時,取所有不在交錯樹中的Y頂點的slack值中的最小值作為d值即可。但還要注意一點:修 改頂標後,要把所有不在交錯樹中的Y頂點的slack值都減去d。"

這段話其實是錯的,請看底下的code:

\ord{N^4}常數優化版本
#define MAXN 100
#define INF INT_MAX
int n;
int g[MAXN][MAXN],lx[MAXN],ly[MAXN],slack_y[MAXN];
int match_y[MAXN];
bool vx[MAXN],vy[MAXN];
bool dfs(int x){
if(vx[x])return 0;
vx[x]=1;
for(int y=0,t;y<n;++y){
if(vy[y])continue;
t=lx[x]+ly[y]-g[x][y];
if(t==0){
vy[y]=1;
if(match_y[y]==-1||dfs(match_y[y])){
match_y[y]=x;
return 1;
}
}else if(slack_y[y]>t)slack_y[y]=t;
}
return 0;
}
inline int km(){
memset(ly,0,sizeof(int)*n);
memset(match_y,-1,sizeof(int)*n);
for(int x=0;x<n;++x){
lx[x]=-INF;
for(int y=0;y<n;++y){
lx[x]=max(lx[x],g[x][y]);
}
}
for(int x=0;x<n;++x){
for(int y=0;y<n;++y)slack_y[y]=INF;
for(;;){
memset(vx,0,sizeof(bool)*n);
memset(vy,0,sizeof(bool)*n);
if(dfs(x))break;
int cut=INF;
for(int y=0;y<n;++y){
if(!vy[y]&&cut>slack_y[y])cut=slack_y[y];
}
for(int j=0;j<n;++j){
if(vx[j])lx[j]-=cut;
if(vy[j])ly[j]+=cut;
else slack_y[j]-=cut;
}
}
}
int ans=0;
for(int y=0;y<n;++y)if(g[match_y[y]][y]!=-INF)ans+=g[match_y[y]][y];
return ans;
}
view raw KM_O(N4).cpp hosted with ❤ by GitHub
我們仔細看一下迴圈的層數。
第一層是一個for迴圈,執行N次;第二層有一個無線迴圈,但她最多執行N次;裡面有一個DFS,最多執行\ord{N^2}次,還有一些迴圈但是不影響複雜度所以不討論。
總複雜度是\ord{N}*\ord{N}*\ord{N^2}\ord{N^4},因此網路上大多數\ord{N^3}的code其實是錯的。

接下來看一下我修改後真正的\ord{N^3}code:
#define MAXN 100
#define INF INT_MAX
int n;
int g[MAXN][MAXN],lx[MAXN],ly[MAXN],slack_y[MAXN];
int match_y[MAXN];//要保證g是完全二分圖
bool vx[MAXN],vy[MAXN];
bool dfs(int x,bool adjust=1){//DFS找增廣路,is=1表示要擴充
if(vx[x])return 0;
vx[x]=1;
for(int y=0;y<n;++y){
if(vy[y])continue;
int t=lx[x]+ly[y]-g[x][y];
if(t==0){
vy[y]=1;
if(match_y[y]==-1||dfs(match_y[y],adjust)){
if(adjust)match_y[y]=x;
return 1;
}
}else if(slack_y[y]>t)slack_y[y]=t;
}
return 0;
}
inline int km(){
memset(match_y,-1,sizeof(int)*n);
memset(ly,0,sizeof(int)*n);
for(int x=0;x<n;++x){
lx[x]=-INF;
for(int y=0;y<n;++y){
lx[x]=max(lx[x],g[x][y]);
}
}
for(int x=0;x<n;++x){
for(int y=0;y<n;++y)slack_y[y]=INF;
memset(vx,0,sizeof(bool)*n);
memset(vy,0,sizeof(bool)*n);
if(dfs(x))continue;
bool flag=1;
while(flag){
int cut=INF;
for(int y=0;y<n;++y){
if(!vy[y]&&cut>slack_y[y])cut=slack_y[y];
}
for(int j=0;j<n;++j){
if(vx[j])lx[j]-=cut;
if(vy[j])ly[j]+=cut;
else slack_y[j]-=cut;
}
for(int y=0;y<n;++y){
if(!vy[y]&&slack_y[y]==0){
vy[y]=1;
if(match_y[y]==-1||dfs(match_y[y],0)){
flag=0;//測試成功,有增廣路
break;
}
}
}
}
memset(vx,0,sizeof(bool)*n);
memset(vy,0,sizeof(bool)*n);
dfs(x);//最後要記得擴充增廣路
}
int ans=0;
for(int y=0;y<n;++y)if(g[match_y[y]][y]!=-INF)ans+=g[match_y[y]][y];
return ans;
}
view raw KM_O(N3).cpp hosted with ❤ by GitHub
可以看到我在dfs裡面加了一個參數adjust,如果他是true,就跟原本的dfs沒兩樣,可以在找到增廣路後順便將增廣路擴充;如果他是false,整個dfs就變成只能判斷有沒有增廣路了。
主函數dfs的部分移到while的外面,而修改lx,ly和slack_y後的地方增加了一個迴圈來判斷這次的修改有沒有產生可行的增廣路,對於新增加的「等邊」,如果他有機會是增廣路的話,會對此「等邊」連像的點y進行dfs「判斷」有沒有增廣路(把adjust設成0)。如果有增廣路,就會跳出while,在進行一次dfs來擴充增廣路。
總複雜度是
\ord{N}[第一層迴圈] * ( \ord{N^2}[dfs的時間] + \ord{N}[while的次數]*\ord{N} ) = \ord{N^3}
這才是真正的\ord{N^3}算法,前者只是常數優化而已

最後是\ord{N^3}算法常數較小的版本:
#define MAXN 100
#define INF INT_MAX
int g[MAXN][MAXN],lx[MAXN],ly[MAXN],slack_y[MAXN];
int px[MAXN],py[MAXN],match_y[MAXN],par[MAXN];
int n;
void adjust(int y){//把增廣路上所有邊反轉
match_y[y]=py[y];
if(px[match_y[y]]!=-2)
adjust(px[match_y[y]]);
}
bool dfs(int x){//DFS找增廣路
for(int y=0;y<n;++y){
if(py[y]!=-1)continue;
int t=lx[x]+ly[y]-g[x][y];
if(t==0){
py[y]=x;
if(match_y[y]==-1){
adjust(y);
return 1;
}
if(px[match_y[y]]!=-1)continue;
px[match_y[y]]=y;
if(dfs(match_y[y]))return 1;
}else if(slack_y[y]>t){
slack_y[y]=t;
par[y]=x;
}
}
return 0;
}
inline int km(){
memset(ly,0,sizeof(int)*n);
memset(match_y,-1,sizeof(int)*n);
for(int x=0;x<n;++x){
lx[x]=-INF;
for(int y=0;y<n;++y){
lx[x]=max(lx[x],g[x][y]);
}
}
for(int x=0;x<n;++x){
for(int y=0;y<n;++y)slack_y[y]=INF;
memset(px,-1,sizeof(int)*n);
memset(py,-1,sizeof(int)*n);
px[x]=-2;
if(dfs(x))continue;
bool flag=1;
while(flag){
int cut=INF;
for(int y=0;y<n;++y)
if(py[y]==-1&&cut>slack_y[y])cut=slack_y[y];
for(int j=0;j<n;++j){
if(px[j]!=-1)lx[j]-=cut;
if(py[j]!=-1)ly[j]+=cut;
else slack_y[j]-=cut;
}
for(int y=0;y<n;++y){
if(py[y]==-1&&slack_y[y]==0){
py[y]=par[y];
if(match_y[y]==-1){
adjust(y);
flag=0;
break;
}
px[match_y[y]]=y;
if(dfs(match_y[y])){
flag=0;
break;
}
}
}
}
}
int ans=0;
for(int y=0;y<n;++y)if(g[match_y[y]][y]!=-INF)ans+=g[match_y[y]][y];
return ans;
}

2016年4月21日 星期四

[ Mersenne Twister ] 梅森旋轉算法

梅森旋轉算法是一種可以快速產生高級偽隨機數的算法,修正了很多以前的隨機速算法的缺陷(Ex:線性同於算法)。

這是專門用在蒙地卡羅測試用的,不要拿它來做持久化隨機二叉數的亂數產生器,會太慢

最為廣泛使用Mersenne Twister的一種變體是MT19937,可以產生32位整數序列,從C++11開始,C++也可以使用這種算法。在Boost C++,Glib和NAG數值庫中,作為插件提供。但是一般在競賽時可能會有不支援的情況發生,所以將它做成模板當作參考。

MT19937_64則是可以產生64位整數序列

以下提供模板:
#ifndef SUNMOON_MERSENNE_TWISTER
#define SUNMOON_MERSENNE_TWISTER
template<typename T,T w,int n,T m,T r,T a,T u,T d,T s,T b,T t,T c,T l,T f>
class mersenne_twister{
private:
int index;
const T lower_mask=(1ull<<r)-1ull;
const T upper_mask=~lower_mask;
T mt[n];
inline void twist(){
for(int i=0;i<n;++i){
T y=(mt[i]&upper_mask)+(mt[(i+1)%n]&lower_mask);
mt[i]=mt[(i+m)%n]^y>>1;
if(y%2!=0)mt[i]=mt[i]^a;
}
index=0;
}
public:
mersenne_twister(T seed):index(n){
mt[0]=seed;
for(int i=1;i<n;++i)mt[i]=f*(mt[i-1]^mt[i-1]>>(w-2))+i;
}
inline T operator()(){
if(index>=n)twist();
T y=mt[index++];
y=y^y>>u&d;
y=y^y<<s&b;
y=y^y<<t&c;
y=y^y>>l;
return y;
}
};
typedef mersenne_twister<unsigned,32,624,397,31,0x9908b0dfUL,11,
0xffffffffUL,7,0x9d2c5680UL,15,0xefc60000UL,18,1812433253UL> MT19937;
typedef mersenne_twister<unsigned long long,64,312,156,31,0xb5026f5aa96619e9ULL,29,0x5555555555555555ULL
,17,0x71d67fffeda60000ULL,37,0xfff7eee000000000ULL,43,6364136223846793005ULL> MT19937_64;
#endif

2016年4月6日 星期三

[ chinese remainder theorem ] 中國剩餘定理

我是看維基百科實現的,所以就直接貼上模板
注意:
  • crt函數的m[i]是模數
  • crt函數的a[i]是原本數模m[i]後的值
  • 必須要保證m兩兩互質
  • 容易溢位
模板:
#ifndef CHINESE_REMAINDER_THEOREM
#define CHINESE_REMAINDER_THEOREM
template<typename T>
inline T Euler(T n){
T ans=n;
for(T i=2;i*i<=n;++i){
if(n%i==0){
ans=ans/i*(i-1);
while(n%i==0)n/=i;
}
}
if(n>1)ans=ans/n*(n-1);
return ans;
}
template<typename T>
inline T pow_mod(T n,T k,T m){
T ans=1;
for(n=(n>=m?n%m:n);k;k>>=1){
if(k&1)ans=ans*n%m;
n=n*n%m;
}
return ans;
}
template<typename T>
inline T crt(std::vector<T> &m,std::vector<T> &a){
T M=1,tM,ans=0;
for(int i=0;i<(int)m.size();++i)M*=m[i];
for(int i=0;i<(int)a.size();++i){
tM=M/m[i];
ans=(ans+(a[i]*tM%M)*pow_mod(tM,Euler(m[i])-1,m[i])%M)%M;
/*如果m[i]是質數,Euler(m[i])-1=m[i]-2,就不用算Euler了*/
}
return ans;
}
#endif

2016年2月25日 星期四

[ dynamic kd tree ] 動態kd樹模板

參考資料:
kd樹的資料在網路上其實滿多的,但很多code效率不高或是功能不全,所以這邊稍微講解一下kd樹基本的一些操作
  1. 構造
    現在有一個point序列S[0~n-1],每個point都包含一個序列d[0~kd-1]表示維度,S[i].d[j]表示第i個點的第j維。
    節點的資料型別定義為node,裡面有l,r兩個指標分別指向左右兩顆子樹,還有一個point pid表示所存的point。
    構造的過程是一個遞迴結構,大致代碼如下:
    node* build(int k,int l,int r){
    if(l>r)return NULL;
    if(k==kd)k=0;
    int mid=(l+r)/2;
    找出S中對第k維排序的中位數M
    並把<M的point排在M左邊,>M的排右邊(相當於std::nth_element()操作)
    node *ret=S[mid];
    ret->l=build(k+1,l,mid-1);
    ret->r=build(k+1,mid+1,r);
    return ret;
    }
    當前這個節點的k稱為分裂維度
  2. 查找
    kd樹支援兩種查找法:
    一、給定一個點p和一個數字k,求離p前k近的點有哪些
    二、給定一個範圍,求範圍內有哪些點
    兩種方法跟一般爆搜有點像,但是利用了kd樹可以做到有效的剪枝,有時候可以判斷被切分出來的範圍內有沒有機會存在前k近點或是在範圍內,直接看模板應該就懂了
  3. 刪除
    模板裡沒有附刪除的code所以會寫在這裡。
    首先如果找到要刪除的節點是葉節點直接刪除就好了;如果不是葉節點,假設現在這個點的分裂維度是k,就拿他右子樹第k維最小節點mi去替代他,接著遞迴刪除右子樹的mi;如果沒有右子樹,就拿他左子樹第k維最小節點mi去替代他,然後把左右子樹互換,接著遞迴刪除右子樹的mi。

    找到要刪除的點用查找中的第一種方法就好了,這裡p=要刪除的點,k=1,查找的時候順便維護最近節點其位置與其對p的距離,若最近距離!=0則刪除失敗。

    那對一個子樹找第k維最小節點呢?方法很簡單,也是遞迴定義的:
    首先如果當前節點o的分裂維度剛好是第k維,則若o有右子樹的話答案必在o的右子樹,否則答案為o,如果o的分裂維度!=k,則遞迴搜尋左右子樹,把得到的答案和o自己進行比較,求最小。
    接下來附上只有刪除操作的模板:
    #ifndef SUNMOON_DYNEMIC_KD_TREE
    #define SUNMOON_DYNEMIC_KD_TREE
    #include<algorithm>
    #include<vector>
    template<typename T,size_t kd>
    class kd_tree{
    public:
    struct point{
    T d[kd];
    inline T dist(const point &x)const{
    T ret=0;
    for(size_t i=0;i<kd;++i)ret+=std::abs(d[i]-x.d[i]);
    return ret;
    }
    inline bool operator==(const point &p){
    for(size_t i=0;i<kd;++i){
    if(d[i]!=p.d[i])return 0;
    }
    return 1;
    }
    inline bool operator<(const point &b)const{
    return d[0]<b.d[0];
    }
    };
    private:
    struct node{
    node *l,*r;
    point pid;
    node(const point &p):l(0),r(0),pid(p){}
    }*root;
    const T INF;
    std::vector<node*> A;
    int s;
    struct __cmp{
    int sort_id;
    inline bool operator()(const node*x,const node*y)const{
    return operator()(x->pid,y->pid);
    }
    inline bool operator()(const point &x,const point &y)const{
    if(x.d[sort_id]!=y.d[sort_id])
    return x.d[sort_id]<y.d[sort_id];
    for(size_t i=0;i<kd;++i){
    if(x.d[i]!=y.d[i])return x.d[i]<y.d[i];
    }
    return 0;
    }
    }cmp;
    void clear(node *o){
    if(!o)return;
    clear(o->l);
    clear(o->r);
    delete o;
    }
    node* build(int k,int l,int r){
    if(l>r)return 0;
    if(k==kd)k=0;
    int mid=(l+r)/2;
    cmp.sort_id=k;
    std::nth_element(A.begin()+l,A.begin()+mid,A.begin()+r+1,cmp);
    node *ret=A[mid];
    ret->l=build(k+1,l,mid-1);
    ret->r=build(k+1,mid+1,r);
    return ret;
    }
    inline T heuristic(const T h[])const{
    T ret=0;
    for(size_t i=0;i<kd;++i)ret+=h[i];
    return ret;
    }
    node *findmin(node*o,int k){
    if(!o)return 0;
    if(cmp.sort_id==k)return o->l?findmin(o->l,(k+1)%kd):o;
    node *l=findmin(o->l,(k+1)%kd);
    node *r=findmin(o->r,(k+1)%kd);
    if(l&&!r)return cmp(l,o)?l:o;
    if(!l&&r)return cmp(r,o)?r:o;
    if(!l&&!r)return o;
    if(cmp(l,r))return cmp(l,o)?l:o;
    return cmp(r,o)?r:o;
    }
    bool erase(node *&u,int k,const point &x){
    if(!u)return 0;
    if(u->pid==x){
    if(u->r);
    else if(u->l){
    u->r=u->l;
    u->l=0;
    }else{
    delete u;
    u=0;
    return 1;
    }
    cmp.sort_id=k;
    u->pid=findmin(u->r,(k+1)%kd)->pid;
    return erase(u->r,(k+1)%kd,u->pid);
    }
    cmp.sort_id=k;
    return erase(cmp(x,u->pid)?u->l:u->r,(k+1)%kd,x);
    }
    void nearest_for_erase(node *&u,int k,const point &x,T *h,T &mndist){
    if(u==0||heuristic(h)>=mndist)return;
    T dist=u->pid.dist(x),old=h[k];
    if(dist<mndist){
    if(!(mndist=dist))return;
    }
    if(x.d[k]<u->pid.d[k]){
    nearest_for_erase(u->l,(k+1)%kd,x,h,mndist);
    h[k]=std::abs(x.d[k]-u->pid.d[k]);
    nearest_for_erase(u->r,(k+1)%kd,x,h,mndist);
    }else{
    nearest_for_erase(u->r,(k+1)%kd,x,h,mndist);
    h[k]=std::abs(x.d[k]-u->pid.d[k]);
    nearest_for_erase(u->l,(k+1)%kd,x,h,mndist);
    }
    h[k]=old;
    }
    public:
    kd_tree(const T &INF):root(0),INF(INF),s(0){}
    inline void clear(){
    clear(root),root=0;
    }
    inline void build(int n,const point *p){
    clear(root),A.resize(s=n);
    for(int i=0;i<n;++i)A[i]=new node(p[i]);
    root=build(0,0,n-1);
    }
    inline bool erase(const point &p){
    return erase(root,0,p);
    }
    inline T nearest(const point &x){
    T mndist=INF,h[kd]={};
    nearest_for_erase(root,0,x,h,mndist);
    return mndist;/*回傳離x最近的點的距離*/
    }
    inline int size(){return s;}
    };
    #endif

插入刪除通常配合替罪羊樹使用,插入很簡單模板有就不說了,接下來講一下複雜度:
設N=size(root)
  • 構造:
    很明顯就是\ord{N \; log \; N}就不說了
  • 查找:
    已經有人證明了,假設現在的維度有k維,則查找的最差複雜度是\ord{N^{1-1/k}}
    但是平均狀況下為\ord{log \; N}
  • 插入:
    複雜度為\ord{樹的高度},因為是套替罪羊樹,而且重構的時間是\ord{N \; log \; N},所以單次插入的均攤時間複雜度是\ord{log \; N \; * \; log \; N}
  • 刪除:
    有三個步驟需要分析,假設現在的維度有k維
    1. findmin:
      最多會遍歷\alpha^{(k-1)*(log_{\alpha}N)/k}=N^{1-1/k}個節點,所以是\ord{N^{1-1/k}}
      但實際操作量為N^{1-1/k}-n^{1-1/k},n是最小值的子樹大小
    2. nearest:
      就是查找操作,所以是\ord{N^{1-1/k}}
      但因為是找相同點,可以優化code,所以實際操作量為N^{1-1/k}-n^{1-1/k},n是相同點的子樹大小
    3. 刪除操作本身:
      看code明顯就是重複findmin直到把刪除的點變成葉節點為止,會感覺做了很多操作,但是我們發現把所有操作加起來後會=\ord{N^{1-1/k}}
      o_1,o_2,...,o_d=findmin(root)找到的點,findmin(o_1)找到的點,...,要刪除的葉節點,n_1,n_2,...,n_d=findmin(root)找到的點的子樹大小,findmin(o_1)找到的點的子樹大小,findmin(o_2)找到的點的子樹大小,...,要刪除的葉節點的子樹大小,d為樹的高度
      複雜度為:
      N^{1-1/k}-n1^{1-1/k}+n1^{1-1/k}-n2^{1-1/k}+n2^{1-1/k}...-nd^{1-1/k}=\ord{N^{1-1/k}}
模板裡的code找的是曼哈頓距離,如果要找歐基里德距離只需要修改point裡的dist跟kd tree裡的heuristic即可,以下提供修改方法:
inline T dist(const point &x)const{
T ret=0;
for(size_t i=0;i<kd;++i)ret+=(d[i]-x.d[i])*(d[i]-x.d[i]);
return ret;
}
inline T heuristic(const T h[])const{
T ret=0;
for(size_t i=0;i<kd;++i)ret+=h[i]*h[i];
return ret;
}
這樣查找時回傳的就是歐基里德距離的平方

以下附只有插入操作的模板:
#ifndef SUNMOON_DYNEMIC_KD_TREE
#define SUNMOON_DYNEMIC_KD_TREE
#include<algorithm>
#include<vector>
#include<queue>
#include<cmath>
template<typename T,size_t kd>//kd表示有幾個維度
class kd_tree{
public:
struct point{
T d[kd];
inline T dist(const point &x)const{
T ret=0;
for(size_t i=0;i<kd;++i)ret+=std::abs(d[i]-x.d[i]);
return ret;
}
inline bool operator<(const point &b)const{
return d[0]<b.d[0];
}
};
private:
struct node{
node *l,*r;
point pid;
int s;
node(const point &p):l(0),r(0),pid(p),s(1){}
inline void up(){
s=(l?l->s:0)+1+(r?r->s:0);
}
}*root;
const double alpha,loga;
const T INF;//記得要給INF,表示極大值
std::vector<node*> A;
int qM;
std::priority_queue<std::pair<T,point > >pQ;
struct __cmp{
int sort_id;
inline bool operator()(const node*x,const node*y)const{
return x->pid.d[sort_id]<y->pid.d[sort_id];
}
}cmp;
void clear(node *o){
if(!o)return;
clear(o->l);
clear(o->r);
delete o;
}
inline int size(node *o){
return o?o->s:0;
}
node* build(int k,int l,int r){
if(l>r)return 0;
if(k==kd)k=0;
int mid=(l+r)/2;
cmp.sort_id=k;
std::nth_element(A.begin()+l,A.begin()+mid,A.begin()+r+1,cmp);
node *ret=A[mid];
ret->l=build(k+1,l,mid-1);
ret->r=build(k+1,mid+1,r);
ret->up();
return ret;
}
inline bool isbad(node*o){
return size(o->l)>alpha*o->s||size(o->r)>alpha*o->s;
}
void flatten(node *u,typename std::vector<node*>::iterator &it){
if(!u)return;
flatten(u->l,it);
*it=u;
flatten(u->r,++it);
}
bool insert(node*&u,int k,const point &x,int dep){
if(!u){
u=new node(x);
return dep<=0;
}
++u->s;
if(insert(x.d[k]<u->pid.d[k]?u->l:u->r,(k+1)%kd,x,dep-1)){
if(!isbad(u))return 1;
if((int)A.size()<u->s)A.resize(u->s);
typename std::vector<node*>::iterator it=A.begin();
flatten(u,it);
u=build(k,0,u->s-1);
}
return 0;
}
inline T heuristic(const T h[])const{
T ret=0;
for(size_t i=0;i<kd;++i)ret+=h[i];
return ret;
}
void nearest(node *u,int k,const point &x,T *h,T &mndist){
if(u==0||heuristic(h)>=mndist)return;
T dist=u->pid.dist(x),old=h[k];
/*mndist=std::min(mndist,dist);*/
if(dist<mndist){
pQ.push(std::make_pair(dist,u->pid));
if((int)pQ.size()==qM+1){
mndist=pQ.top().first,pQ.pop();
}
}
if(x.d[k]<u->pid.d[k]){
nearest(u->l,(k+1)%kd,x,h,mndist);
h[k]=std::abs(x.d[k]-u->pid.d[k]);
nearest(u->r,(k+1)%kd,x,h,mndist);
}else{
nearest(u->r,(k+1)%kd,x,h,mndist);
h[k]=std::abs(x.d[k]-u->pid.d[k]);
nearest(u->l,(k+1)%kd,x,h,mndist);
}
h[k]=old;
}
public:
kd_tree(const T &INF,double a=0.75):root(0),alpha(a),loga(log2(1.0/a)),INF(INF){}
inline void clear(){
clear(root),root=0;
}
inline void build(int n,const point *p){
clear(root),A.resize(n);
for(int i=0;i<n;++i)A[i]=new node(p[i]);
root=build(0,0,n-1);
}
inline void insert(const point &x){
insert(root,0,x,std::__lg(size(root))/loga);
}
inline T nearest(const point &x,int k){
qM=k;
T mndist=INF,h[kd]={};
nearest(root,0,x,h,mndist);
mndist=pQ.top().first;
pQ=std::priority_queue<std::pair<T,point > >();
return mndist;/*回傳離x第k近的點的距離*/
}
inline int size(){return root?root->s:0;}
};
#endif

以下附支援插入刪除的模板:
#ifndef SUNMOON_DYNEMIC_KD_TREE
#define SUNMOON_DYNEMIC_KD_TREE
#include<algorithm>
#include<vector>
#include<queue>
#include<cmath>
template<typename T,size_t kd>//kd表示有幾個維度
class kd_tree{
public:
struct point{
T d[kd];
inline T dist(const point &x)const{
T ret=0;
for(size_t i=0;i<kd;++i)ret+=std::abs(d[i]-x.d[i]);
return ret;
}
inline bool operator==(const point &p){
for(size_t i=0;i<kd;++i){
if(d[i]!=p.d[i])return 0;
}
return 1;
}
inline bool operator<(const point &b)const{
return d[0]<b.d[0];
}
};
private:
struct node{
node *l,*r;
point pid;
int s;
node(const point &p):l(0),r(0),pid(p),s(1){}
inline void up(){
s=(l?l->s:0)+1+(r?r->s:0);
}
}*root;
const double alpha,loga;
const T INF;//記得要給INF,表示極大值
int maxn;
struct __cmp{
int sort_id;
inline bool operator()(const node*x,const node*y)const{
return operator()(x->pid,y->pid);
}
inline bool operator()(const point &x,const point &y)const{
if(x.d[sort_id]!=y.d[sort_id])
return x.d[sort_id]<y.d[sort_id];
for(size_t i=0;i<kd;++i){
if(x.d[i]!=y.d[i])return x.d[i]<y.d[i];
}
return 0;
}
}cmp;
void clear(node *o){
if(!o)return;
clear(o->l);
clear(o->r);
delete o;
}
inline int size(node *o){
return o?o->s:0;
}
std::vector<node*> A;
node* build(int k,int l,int r){
if(l>r)return 0;
if(k==kd)k=0;
int mid=(l+r)/2;
cmp.sort_id=k;
std::nth_element(A.begin()+l,A.begin()+mid,A.begin()+r+1,cmp);
node *ret=A[mid];
ret->l=build(k+1,l,mid-1);
ret->r=build(k+1,mid+1,r);
ret->up();
return ret;
}
inline bool isbad(node*o){
return size(o->l)>alpha*o->s||size(o->r)>alpha*o->s;
}
void flatten(node *u,typename std::vector<node*>::iterator &it){
if(!u)return;
flatten(u->l,it);
*it=u;
flatten(u->r,++it);
}
inline void rebuild(node*&u,int k){
if((int)A.size()<u->s)A.resize(u->s);
typename std::vector<node*>::iterator it=A.begin();
flatten(u,it);
u=build(k,0,u->s-1);
}
bool insert(node*&u,int k,const point &x,int dep){
if(!u){
u=new node(x);
return dep<=0;
}
++u->s;
cmp.sort_id=k;
if(insert(cmp(x,u->pid)?u->l:u->r,(k+1)%kd,x,dep-1)){
if(!isbad(u))return 1;
rebuild(u,k);
}
return 0;
}
node *findmin(node*o,int k){
if(!o)return 0;
if(cmp.sort_id==k)return o->l?findmin(o->l,(k+1)%kd):o;
node *l=findmin(o->l,(k+1)%kd);
node *r=findmin(o->r,(k+1)%kd);
if(l&&!r)return cmp(l,o)?l:o;
if(!l&&r)return cmp(r,o)?r:o;
if(!l&&!r)return o;
if(cmp(l,r))return cmp(l,o)?l:o;
return cmp(r,o)?r:o;
}
bool erase(node *&u,int k,const point &x){
if(!u)return 0;
if(u->pid==x){
if(u->r);
else if(u->l){
u->r=u->l;
u->l=0;
}else{
delete u;
u=0;
return 1;
}
--u->s;
cmp.sort_id=k;
u->pid=findmin(u->r,(k+1)%kd)->pid;
return erase(u->r,(k+1)%kd,u->pid);
}
cmp.sort_id=k;
if(erase(cmp(x,u->pid)?u->l:u->r,(k+1)%kd,x)){
--u->s;return 1;
}else return 0;
}
inline T heuristic(const T h[])const{
T ret=0;
for(size_t i=0;i<kd;++i)ret+=h[i];
return ret;
}
int qM;
std::priority_queue<std::pair<T,point > >pQ;
void nearest(node *u,int k,const point &x,T *h,T &mndist){
if(u==0||heuristic(h)>=mndist)return;
T dist=u->pid.dist(x),old=h[k];
/*mndist=std::min(mndist,dist);*/
if(dist<mndist){
pQ.push(std::make_pair(dist,u->pid));
if((int)pQ.size()==qM+1){
mndist=pQ.top().first,pQ.pop();
}
}
if(x.d[k]<u->pid.d[k]){
nearest(u->l,(k+1)%kd,x,h,mndist);
h[k]=std::abs(x.d[k]-u->pid.d[k]);
nearest(u->r,(k+1)%kd,x,h,mndist);
}else{
nearest(u->r,(k+1)%kd,x,h,mndist);
h[k]=std::abs(x.d[k]-u->pid.d[k]);
nearest(u->l,(k+1)%kd,x,h,mndist);
}
h[k]=old;
}
std::vector<point>in_range;
void range(node *u,int k,const point&mi,const point&ma){
if(!u)return;
bool is=1;
for(int i=0;i<kd;++i)
if(u->pid.d[i]<mi.d[i]||ma.d[i]<u->pid.d[i]){
is=0;break;
}
if(is)in_range.push_back(u->pid);
if(mi.d[k]<=u->pid.d[k])range(u->l,(k+1)%kd,mi,ma);
if(ma.d[k]>=u->pid.d[k])range(u->r,(k+1)%kd,mi,ma);
}
public:
kd_tree(const T &INF,double a=0.75):root(0),alpha(a),loga(log2(1.0/a)),INF(INF),maxn(1){}
inline void clear(){
clear(root),root=0,maxn=1;
}
inline void build(int n,const point *p){
clear(root),A.resize(maxn=n);
for(int i=0;i<n;++i)A[i]=new node(p[i]);
root=build(0,0,n-1);
}
inline void insert(const point &x){
insert(root,0,x,std::__lg(size(root))/loga);
if(root->s>maxn)maxn=root->s;
}
inline bool erase(const point &p){
bool d=erase(root,0,p);
if(root&&root->s<alpha*maxn)rebuild();
return d;
}
inline void rebuild(){
if(root)rebuild(root,0);
maxn=root->s;
}
inline T nearest(const point &x,int k){
qM=k;
T mndist=INF,h[kd]={};
nearest(root,0,x,h,mndist);
mndist=pQ.top().first;
pQ=std::priority_queue<std::pair<T,point > >();
return mndist;/*回傳離x第k近的點的距離*/
}
inline const std::vector<point> &range(const point&mi,const point&ma){
in_range.clear();
range(root,0,mi,ma);
return in_range;/*回傳介於mi到ma之間的點vector*/
}
inline int size(){return root?root->s:0;}
};
#endif
view raw kd_tree.cpp hosted with ❤ by GitHub
模板的使用方法請參考:

日月卦長的解題紀錄 [ IOICAMP2016 ] 動態曼哈頓最短距離


2016年1月31日 星期日

[ Miller-Rabin Strong Probable-prime Base ] 質數測試算法保證long long範圍內正確

這本來是隨機演算法,但是已經有一些人,找出一些特定底數,保證判定結果在某些範圍內正確。

如果是unsigned long long範圍內東西記得要用long long乘long long mod long long 的模板
這裡的範例已經加在code裡了,如果不需要可以把它拿掉(zerojudge a007不拿掉會變慢)

code:
inline long long mod_mul(long long a,long long b,long long m){
a%=m,b%=m;
long long y=(long long)((double)a*b/m+0.5);/* fast for m < 2^58 */
long long r=(a*b-y*m)%m;
return r<0?r+m:r;
}
template<typename T>
inline T pow(T a,T b,T mod){//a^b%mod
T ans=1;
for(;b;a=mod_mul(a,a,mod),b>>=1)
if(b&1)ans=mod_mul(ans,a,mod);
return ans;
}
int sprp[3]={2,7,61};//int範圍可解
int llsprp[7]={2,325,9375,28178,450775,9780504,1795265022};//至少unsigned long long範圍
template<typename T>
inline bool isprime(T n,int *sprp,int num){
if(n==2)return 1;
if(n<2||n%2==0)return 0;
int t=0;
T u=n-1;
for(;u%2==0;++t)u>>=1;
for(int i=0;i<num;++i){
T a=sprp[i]%n;
if(a==0||a==1||a==n-1)continue;
T x=pow(a,u,n);
if(x==1||x==n-1)continue;
for(int j=1;j<t;++j){
x=mod_mul(x,x,n);
if(x==1)return 0;
if(x==n-1)break;
}
if(x==n-1)continue;
return 0;
}
return 1;
}

2016年1月27日 星期三

[ Biconnected Component ] 雙連通分量(BCC)

一張無向圖上,不會產生割點的連通分量,稱作「雙連通分量」。一張無向圖的雙連通分量們,通常會互相重疊,重疊的部分都是原圖的割點。
可以利用堆疊+tarjan算法快速地找出那些點是割點和雙連通分量
以下提供模板:
#include<vector>
#include<algorithm>
#define N 1005
std::vector<int> G[N];// 1-base
std::vector<int> bcc[N];//存每塊雙連通分量的點
int low[N],vis[N],Time;
int bcc_id[N],bcc_cnt;// 1-base
bool is_cut[N];//是否為割點,割點的bcc_id沒意義
int st[N],top;
void dfs(int u,int pa=-1){//u當前點,pa父親
int v,child=0;
low[u]=vis[u]=++Time;
st[top++]=u;
for(size_t i=0;i<G[u].size();++i){
if(!vis[v=G[u][i]]){
dfs(v,u),++child;
low[u]=std::min(low[u],low[v]);
if(vis[u]<=low[v]){
is_cut[u]=1;
bcc[++bcc_cnt].clear();
int t;
do{
bcc_id[t=st[--top]]=bcc_cnt;
bcc[bcc_cnt].push_back(t);
}while(t!=v);
bcc_id[u]=bcc_cnt;
bcc[bcc_cnt].push_back(u);
}
}else if(vis[v]<vis[u]&&v!=pa)//反向邊
low[u]=std::min(low[u],vis[v]);
}
if(pa==-1&&child<2)is_cut[u]=0;//u是dfs樹的根要特判
}
inline void bcc_init(int n){
Time=bcc_cnt=top=0;
for(int i=1;i<=n;++i){
G[i].clear();
vis[i]=0;
is_cut[i]=0;
bcc_id[i]=0;
}
}
view raw BC.cpp hosted with ❤ by GitHub

2016年1月3日 星期日

[ Strongly Connected Component - Tarjan & Kosaraju ] 強連通分量的兩種做法(dfs)

一張有向圖,找到所有的 Strongly Connected Component ;亦可進一步收縮所有的 SCC 、收縮所有的環,讓原圖變成 DAG 。

方法1-Tarjan法
跟BCC差不多,直接提供模板:
#include<vector>
#include<algorithm>
#define N 50005
std::vector<int> s[N];
int low[N],v[N]={0},Time=0,ans=0,cnt=0;
int st[N],top=0,contract[N];
bool instack[N]={0};
void dfs(int x){
low[x]=v[x]=++Time;
st[top++]=x;
instack[x]=1;
for(int i=0,r;i<(int)s[x].size();++i){
if(!v[r=s[x][i]])dfs(r);
if(instack[r])low[x]=std::min(low[x],low[r]);
}
if(v[x]==low[x]){
int u;
do{
instack[u=st[--top]]=0;
contract[u]=cnt;/*每個點所在的SCC*/
}while(u!=x);
++cnt;
}
}
view raw SCC_tarjan.cpp hosted with ❤ by GitHub

方法2-Kosaraju
對圖做一次DFS,求他的時間戳,再把圖反向,逆著時間戳進行第二次DFS,所遍歷的點就會是同一個SCC,記得不要走重複的點
以下提供模板:
#include<vector>
#include<algorithm>
#define N 1000005
std::vector<int >s[N],g[N];/*g是反向圖,要先做好*/
bool v[N]={0};
int st[N],top=0,contract[N]={0};
void dfs(int x){
v[x]=1;
for(int i=0;i<(int)s[x].size();++i){
if(!v[s[x][i]])dfs(s[x][i]);
}
st[top++]=x;
}
void dfs2(int x,int k){
if(contract[x])return;
contract[x]=k;/*x屬於第k個contract*/
for(int i=0;i<(int)g[x].size();++i){
dfs2(g[x][i],k);
}
}
view raw SCC_DFS.cpp hosted with ❤ by GitHub

用法:
int n,m;
int a,b;
int main(){
scanf("%d%d",&n,&m);/*n個點m條邊*/
while(m--){
scanf("%d%d",&a,&b);
s[a].push_back(b);
g[b].push_back(a);/*反向圖*/
}
for(int i=0;i<n;++i){
if(!v[i])dfs(i);/*第一次DFS*/
}
for(int i=0;i<top;++i)printf("%d ",st[i]);puts("");
for(int i=top-1,t=0;i>=0;--i){/*反著處理*/
if(!contract[st[i]])dfs2(st[i],++t);
}
for(int i=0;i<n;i++)printf("%d ",contract[i]);
return 0;
}
view raw SCC_DFS_use.cpp hosted with ❤ by GitHub

[ Bridge & Bridge-connected Component ] 橋和橋連通分量(BCC)

只要從一張無向圖上移除了,就會讓這張圖分離成更多部分,呈現不連通的狀態。
注意!必須要考慮重邊的問題:
  1. 對於有向圖的強連通分量,重邊是沒有影響的,因為強連通只要求任意兩點可以互相連通
  2. 對於無向圖的點雙連通分量,重邊也是沒有影響的,因為點雙連通要求是任意兩點之間至少存在兩條點不重複的路徑,對邊的重複並沒有要求
  3. 對於無向圖的邊雙連通分量,重邊就有影響了,因為邊雙連通要求任意兩點之間至少存在兩條邊不重複的路徑
這裡提供模板:
#include<vector>
#include<algorithm>
#define N 1005
struct edge{
int u,v;
bool is_bridge;
edge(int u=0,int v=0):u(u),v(v),is_bridge(0){}
};
std::vector<edge> E;
std::vector<int> G[N];// 1-base
int low[N],vis[N],Time,bridge_cnt;
inline void add_edge(int u,int v){
G[u].push_back(E.size());
E.push_back(edge(u,v));
G[v].push_back(E.size());
E.push_back(edge(v,u));
}
void dfs(int u,int re=-1){//u當前點,re為u連接前一個點的邊
int v;
low[u]=vis[u]=++Time;
st[top++]=u;
for(size_t i=0;i<G[u].size();++i){
int e=G[u][i];v=E[e].v;
if(!vis[v]){
dfs(v,e^1);//e^1反向邊
low[u]=std::min(low[u],low[v]);
if(vis[u]<low[v]){
E[e].is_bridge=E[e^1].is_bridge=1;
++bridge_cnt;
}
}else if(vis[v]<vis[u]&&e!=re)
low[u]=std::min(low[u],vis[v]);
}
}
view raw bridge.cpp hosted with ❤ by GitHub

一張無向圖上,不會產生橋的連通分量,稱作橋連通分量,或是橋雙連通分量
這裡提供模板,code跟橋差不多,只是需要用堆疊去記錄當前節點而已:

#include<vector>
#include<algorithm>
#define N 1005
struct edge{
int u,v;
bool is_bridge;
edge(int u=0,int v=0):u(u),v(v),is_bridge(0){}
};
std::vector<edge> E;
std::vector<int> G[N];// 1-base
int low[N],vis[N],Time;
int bcc_id[N],bridge_cnt,bcc_cnt;// 1-base
int st[N],top;//BCC用
inline void add_edge(int u,int v){
G[u].push_back(E.size());
E.push_back(edge(u,v));
G[v].push_back(E.size());
E.push_back(edge(v,u));
}
void dfs(int u,int re=-1){//u當前點,re為u連接前一個點的邊
int v;
low[u]=vis[u]=++Time;
st[top++]=u;
for(size_t i=0;i<G[u].size();++i){
int e=G[u][i];v=E[e].v;
if(!vis[v]){
dfs(v,e^1);//e^1反向邊
low[u]=std::min(low[u],low[v]);
if(vis[u]<low[v]){
E[e].is_bridge=E[e^1].is_bridge=1;
++bridge_cnt;
}
}else if(vis[v]<vis[u]&&e!=re)
low[u]=std::min(low[u],vis[v]);
}
if(vis[u]==low[u]){//處理BCC
++bcc_cnt;// 1-base
do bcc_id[v=st[--top]]=bcc_cnt;//每個點所在的BCC
while(v!=u);
}
}
inline void bcc_init(int n){
Time=bcc_cnt=bridge_cnt=top=0;
E.clear();
for(int i=1;i<=n;++i){
G[i].clear();
vis[i]=0;
bcc_id[i]=0;
}
}
view raw BCC.cpp hosted with ❤ by GitHub

[ Articulation Vertex ] 割點

關節點(割點)是讓一張無向圖維持連通,不可或缺的點。只要從一張無向圖上移除了關節點(以及與之相連的邊),就會讓這張圖分離成更多部分,呈現不連通的狀態。
存在一種線性算法找出一張圖所有的割點,一個dfs就能解決了

以下提供模板:
#include<vector>
#include<algorithm>
#define N 50005
std::vector<int> s[N];
int low[N],v[N]={0},Time=0,ans=0;
void dfs(int x,int p){/*x當前點,p父親*/
int i,r,is=0,add=0;
low[x]=v[x]=++Time;
for(i=0;i<(int)s[x].size();++i){
if(!v[r=s[x][i]]){
dfs(r,x),++add;
low[x]=std::min(low[x],low[r]);
if(v[x]<=low[r])is=1;
}
if(r!=p)low[x]=std::min(low[x],v[r]);
}/*傳回有幾個割點*/
if(is)++ans;
if(x==p&&add<2)--ans;/*x是dfs樹的根要特判*/
}