集訓
DAY1 搜尋進階
因此在學習的時候主要以程式碼實踐為主(多做題)
深度優先搜尋(dfs)基礎
1.子集列舉
複雜度\(O(2^n)\)
2.排列列舉
複雜度\(O(n!)\)
Dfs的剪枝
1.最佳化搜尋順序
sort
列舉順序(正/倒)
2.排除等效冗餘
inline void dfs(int i,int len,int st){//P1120
if(i==cnt+1)...
if(len==tar)...
for(int j=st/*限制單調*/;j<=n;j++){
if(!vis[j]&&len+a[j]<=tar){
...
if(len==0||len+a[j]==tar)return;//排除等效冗餘
j=nxt[j];
}
}
}
int main(){
...
nxt[n]=n;
for(int i=n-1;i>=1;i--){//避免重複
if(a[i]!=a[i+1])nxt[i]=i;
else nxt[i]=nxt[i+1];
}
...
}
3.可行性剪枝(上下界剪枝) 當前分支必定失敗
void dfs(int i,int lh,int lr,int sums,int sumv){//P1731 //P1025
...
if(sumv>=n)return;//可行性
if(i*lr*lr*lh+sumv<n)return;
if(i*2+sums>=ans)return;
for(int j=lr-1;j>=i;j--)//上下界
for(int k=lh-1;k>=i;k--)
...
}
4.最優性剪枝 答案不會更優
if(...)return;//P10483 P1731
5.記憶化
迭代加深
搜尋規模隨搜尋層數深入增長很快,且能夠確保答案在較淺節點
思想:限制深度
bool dfs(int i){
if(i==dep){
return ...;
}
for...{if(...){return 1;}}
return 0;
}
void solve(){
dep=1;
while(!dfs(1)){
dep++;
}
}//uva529
P1763
hack
雙向搜尋
具有明確的“初始狀態”和“終止狀態”
從初態和終態出發各搜一半狀態,產生兩個深度減半的狀態空間,在中間交會、組合成最終答案。\(O(2^\frac{n}{2})\)
//CF888E U311043
void dfs1(int i)...
void dfs2(int i)...
void solve()...
int main(){
dfs1();dfs2();solve();
}
廣度優先搜尋(BFS)基礎
U311287
大模擬
struct傳參太多會TLE
3維狀態
U311289
多個源點同時入隊
BFS變形
1.雙端佇列BFS(0/1bfs)
deque
0費入隊首,1費入隊尾
2.優先佇列BFS
uva11367
定義狀態\((city,cost,left)\)
dijkstra轉移狀態
優先佇列使得\(cost\)最小
二維狀態判重
void solve(){
int c=read(),s=read()+1,e=read()+1;
memset(vis,0,sizeof vis);
memset(f,0x3f,sizeof(f));
f[s][0]=0;
priority_queue<node>q;
q.push(node{s,0,0});
while(!q.empty()){
node now=q.top();
q.pop();
if(now.u==e){
cout<<now.cot<<"\n";
return;
}
if(vis[now.u][now.res])continue;
else vis[now.u][now.res]=1;
if(now.res<c&&f[now.u][now.res+1]>now.cot+val[now.u]){
f[now.u][now.res+1]=now.cot+val[now.u];
q.push(node{now.u,f[now.u][now.res+1],now.res+1});
}
for(int j=0;j<(int)a[now.u].size();j++){
int v=a[now.u][j].v,w=a[now.u][j].w;
if(now.res>=w&&f[v][now.res-w]>now.cot){
f[v][now.res-w]=now.cot;
q.push(node{v,f[v][now.res-w],now.res-w});
}
}
}
puts("impossible");
}
3.雙向BFS
acwing177
大量細節
開兩個佇列q1,q2
每單位時間q1擴充三次,q2一次
若兩者有相同可到達點即輸出當前時間
擴充時打上第幾次標記
擴充時打vis標記
多測清空
判段是否在鬼區內
void bfs(){
int tim=0,pre=1,pre2=1;
memset(vis,0,sizeof(vis));
memset(vis2,0,sizeof(vis2));
queue<node>q1;
queue<node>q2;
q1.push(node{bx,by,1});
q2.push(node{gx,gy,1});
while((!q1.empty())&&(!q2.empty())){
tim++;
for(int i=1;i<=3;i++){
while(!q1.empty()&&q1.front().t==pre){
node now=q1.front();
q1.pop();
if(!check(now.x,now.y,tim))continue;
for(int j=0;j<4;j++){
int tx=now.x+dx[j],ty=now.y+dy[j];
if(tx>0&&tx<=n&&ty>0&&ty<=m&&check(tx,ty,tim)&&a[tx][ty]!='X'){
if(vis2[tx][ty]){
cout<<tim<<"\n";
return;
}
if(vis[tx][ty])continue;
else vis[tx][ty]=1;
q1.push(node{tx,ty,pre+1});
}
}
}
pre++;
}
while(!q2.empty()&&q2.front().t==pre2){
node now=q2.front();
q2.pop();
if(!check(now.x,now.y,tim))continue;
for(int j=0;j<4;j++){
int tx=now.x+dx[j],ty=now.y+dy[j];
if(tx>0&&tx<=n&&ty>0&&ty<=m&&check(tx,ty,tim)&&a[tx][ty]!='X'){
if(vis[tx][ty]){
cout<<tim<<"\n";
return;
}
if(vis2[tx][ty])continue;
else vis2[tx][ty]=1;
q2.push(node{tx,ty,pre2+1});
}
}
}
pre2++;
}
puts("-1");
}
DAY2-3 資料結構提高
單調棧
//P5788
stack<int>st;
for(int i=1;i<=n;i++){
while(!st.empty()&&h[i]>h[st.top()])st.pop();
st.push(i);
}
while(!st.empty())st.pop();
分治+單調棧
擺
某計蒜客題
擺
P1823
在單調棧內二分
SP1805
經典
單調佇列
deque<int>q;
for(int i=1;i<=n;i++){
while(!q.empty()&&q.front()<i-m)q.pop_front();
...
while(!q.empty()&&a[q.back()]>=a[i])q.pop_back();
q.push_back(i);
}//P1440
P2216
二維
堆
void push(int x){//P3378
heap[++len]=x;
int i=len;
while(i>1&&heap[i/2]>heap[i]){
swap(heap[i/2],heap[i]);
i=i/2;
}
}
void pop(){
heap[1]=heap[len--];
int i=1;
while(i*2<=len){
int son=i*2;
if(son+1<=len&&heap[son]>heap[son+1])son++;
if(heap[son]<heap[i]){
swap(heap[son],heap[i]);
i=son;
}else break;
}
}
int ask(){return heap[1];}
左偏樹
\(dist\)定義:一個節點到子樹中最近的外節點所經過的邊的數量,\(dist[0]=-1\)
維護\(dist\)以維護左偏性質
左偏樹的深度沒有保證
int find(int i){return f[i]==i?i:f[i]=find(f[i]);}//P3377
int merge(int i,int j){
if(!i||!j)return i|j;//節點為0
if(a[j].val<a[i].val)swap(i,j);//維護堆
a[i].rs=merge(a[i].rs,j);//遞迴合併右子和另一個堆
if(a[a[i].rs].dist>a[a[i].ls].dist)swap(a[i].ls,a[i].rs);//維護左偏
a[i].dist=a[a[i].rs].dist+1;//維護dist
return i;//返回節點
}
void init(){
a[0].dist=-1;
scanf("%d",&a[i].val);
a[i].id=f[i]=i;
a[i].ls=a[i].rs=a[i].dist=a[i].dead=0;
}
int main(){
//合併 插入
if(a[x].dead||a[y].dead)continue;
int fx=find(x),fy=find(y);
if(fx!=fy)f[fx]=f[fy]=merge(fx,fy);
//刪除根
if(a[x].dead)continue;
int fx=find(x);a[fx].dead=1;
f[fx]=f[a[fx].ls]=f[a[fx].rs]=merge(a[fx].ls,a[fx].rs);
}
P4331
擺
基礎 STL
注意:
1.區間左閉右開 [,)
2.deque->MLE
pair
預設排序先first後second
vector
t.emplace_back()//新插入方式
t.resize()//重置大小
//定點插入刪除元素
v.insert(a.begin()+i,j)//在v的第i個元素位置插入j(從0開始)
v.erase(a.begin(),a.begin()+i)//刪除 v 的 第0~第i-1個元素
set (multiset)
去重(不去重)
s.erase(pos)//刪除pos迭代器所指向的元素,返回下一個迭代器的位置
s.erase(x)//為刪除set中值為x的元素
//注意:multiset執行該操作會刪除所有=x的元素,如果只想刪除一個則應用s.erase(s.find(x))
s.find(x)//查詢 x 元素是否存在,如果存在返回該元素的迭代器,若不存在,返回s.end(),用法 auto pos=s.find(x)
s.count(x)//查詢 x 元素的個數,用於 multiset,複雜度很高,為O(ans+logn)
s.lower_bound(x)//返回第一個 >= x 的元素的迭代器
s.upper_bound(x)//返回第一個 > x 的元素的迭代器
nth_element()
平均O(n)的時間複雜度,將陣列中第k小的元素放到第k個位置上,同時保證左邊元素全部小於它,右邊元素全部大於它。
//用法 (a陣列下標從1開始,將第i小的元素放到第i個位置)
nth_element(a+1,a+i,a+n+1,cmp);
lower_bound()
返回第一個>=x位置下標
upper_bound()
返回第一個>x位置下標
使用前保證單調性!sort
//用法
int p=lower_bound(a+1,a+n+1,x,cmp);
map
map<T,T>mp;//T必須已定義大小比較
bitset
最佳化傳遞閉包,揹包 \(O(\frac{n}{w})\)
bitset<size>b;
樹狀陣列
1.單點修改,區間查詢
int lowbit(int i){return i&(-i);}//P3374
int sum(int i){
int ret=0;
while(i){
ret+=c[i];
i-=lowbit(i);
}
return ans;
}
void add(int i,int val){
while(i<=n){
c[i]+=val;
i+=lowbit(i);
}
}
2.區間修改,單點查詢
維護原序列的差分序列
線段樹
注意:四倍空間
void pushup(int p){
...
}
void update(int p,int pl,int pr,...){
if(pl==pr){
...
return;
}
if(...)update(ls,pl,mid,...);
if(...)update(rs,mid+1,pr,...);
pushup(p);
}
int query(int l,int r,int p,int pl,int pr){
if(l<=pl&&pr<=r){
return ...;
}
int ret=0;
if(l<=mid)...
if(r>mid)...
return ret;
}
P4513
經典
LOJ6029
除法轉化為減法,暴力更新
void updatediv(int l,int r,int p,int pl,int pr,int k){
if(l<=pl&&pr<=r){
ll n1=floor(1.0*t[p].maxn/k)-t[p].maxn;
ll n2=floor(1.0*t[p].minn/k)-t[p].minn;
if(pl==pr){
t[p].sum=t[p].maxn=t[p].minn=floor(1.0*t[p].minn/k);
}else if(n1==n2){
t[p].tag+=n1;
t[p].maxn+=n1;
t[p].minn+=n1;
t[p].sum+=n1*(pr-pl+1);
}else{
pushdown(p,pl,pr);
updatediv(l,r,ls,pl,mid,k);
updatediv(l,r,rs,mid+1,pr,k);
pushup(p);
}
return;
}
pushdown(p,pl,pr);
if(l<=mid)updatediv(l,r,ls,pl,mid,k);
if(r>mid)updatediv(l,r,rs,mid+1,pr,k);
pushup(p);
}
不開long long見祖宗
P3369
值域線段樹&動態開點線段樹
void pushup(int p){
val[p]=val[ls]+val[rs];
}
void add(int &p,int pl,int pr,int pos,int k){//數pos的個數增加k
if(!p)p=++cnt;
if(pl==pr){
val[p]+=k;
return;
}
if(pos<=mid)add(ls,pl,mid,pos,k);
else if(pos>mid)add(rs,mid+1,pr,pos,k);
pushup(p);
}
int query(int l,int r,int p,int pl,int pr){//返回[l,r]數的個數
if(!p)return 0;
if(l<=pl&&pr<=r)return val[p];
int ans=0;
if(l<=mid)ans+=query(l,r,ls,pl,mid);
if(r>mid)ans+=query(l,r,rs,mid+1,pr);
return ans;
}
int getrank(int k,int p,int pl,int pr){//查詢第k大的數
if(pl==pr)return pl;
if(val[ls]>=k)return getrank(k,ls,pl,mid);
else return getrank(k-val[ls],rs,mid+1,pr);
}
//插入一個數x
add(rt,-lim,lim,x,1);
//刪除一個數x(若有多個相同的數,應只刪除一個)
add(rt,-lim,lim,x,-1);
//定義排名為比當前數小的數的個數+1,查詢x的排名
query(-lim,x-1,rt,-lim,lim)+1
//查詢資料結構中排名為x的數
getrank(x,rt,-lim,lim)
//求x的前驅(前驅定義為小於x,且最大的數)
int rk=query(-lim,x-1,rt,-lim,lim);
getrank(rk,rt,-lim,lim)
//求x的後繼(後繼定義為大於x,且最小的數)
int rk=query(-lim,x,rt,-lim,lim)+1;
getrank(rk,rt,-lim,lim)
P4198
序列\(a\),可修改,查詢滿足\({\forall}j{\in}[1,i-1],a[i]>a[j]\)的\(i\)個數
複雜度\(O(nlog^2n)\)
int query(int p,int pl,int pr,double K){//返回p節點內>k的個數
if(pl==pr){
return t[p].maxn>K;
}
if(t[ls].maxn>K)return query(ls,pl,mid,K)+t[p].len-t[ls].len;//遞迴左子+右子貢獻
else return query(rs,mid+1,pr,K);//左子無貢獻,遞迴右子
}
void pushup(int p,int pl,int pr){
t[p].maxn=max(t[ls].maxn,t[rs].maxn);
t[p].len=t[ls].len+query(rs,mid+1,pr,t[ls].maxn);//左子不受影響+右子>左子最大值
}
void update(int pos,int p,int pl,int pr,double K){
if(pl==pr){
t[p].len=1;
t[p].maxn=K;
return;
}
if(pos<=mid)update(pos,ls,pl,mid,K);
else update(pos,rs,mid+1,pr,K);
pushup(p,pl,pr);
}
update(x,1,1,n,tmp);
t[1].len
P4556
值域線段樹+樹上差分+線段樹合併
int merge(int p,int i,int pl,int pr){//合併
if(!p||!i)return p|i;
if(pl==pr){
...
return p;
}
//合併左右子樹
ls=ch[i][0]=merge(ls,ch[i][0],pl,mid);
rs=ch[i][1]=merge(rs,ch[i][1],mid+1,pr);
pushup(p);
return p;
}
void getans(int i,int fa){//自下而上合併
for(int j=0;j<a[i].size();j++){
int v=a[i][j];
if(v!=fa){
getans(v,i);
rt[i]=rt[v]=merge(rt[i],rt[v],1,1e5);//更改根節點
}
}
//統計答案
ans[i]=maxn[rt[i]];
if(!sum[rt[i]])ans[i]=0;//特判
}
P4197
線段樹合併
在每個點建一顆線段樹,加入離散化後的點權
離線詢問,按\(x\)排序
將邊按邊權排序,列舉每個詢問,加邊,合併線段樹
注意陣列大小
輸出回車
CF786B
線段樹最佳化建圖
一棵入樹,一棵出樹,節點標號偏移\(D\)
\(D\)由資料範圍確定
void build(int p,int pl,int pr){
if(pl==pr){
a[pl]=p;//獲取節點i對應的樹上節點a[i]
return;
}
//出樹向父節點連邊
e[ls+D].push_back(edge{p+D,0});
e[rs+D].push_back(edge{p+D,0});
//入樹向子節點連邊
e[p].push_back(edge{ls,0});
e[p].push_back(edge{rs,0});
build(ls,pl,mid);
build(rs,mid+1,pr);
}
void connect(int l,int r,int v,ll w,int p,int pl,int pr,int op){
if(l<=pl&&pr<=r){
//出樹向入樹連邊
if(op)e[p+D].push_back(edge{a[v],w});
else e[a[v]/*注意轉換*/+D].push_back(edge{p,w});
return;
}
if(l<=mid)connect(l,r,v,w,ls,pl,mid,op);
if(r>mid)connect(l,r,v,w,rs,mid+1,pr,op);
}
void init(){
build(1,1,n);
for(int i=1;i<=n;i++){
//出樹和入樹的葉子節點是同一節點
e[a[i]/*注意轉換*/].push_back(edge{a[i]/*注意轉換*/+D,0});
e[a[i]/*注意轉換*/+D].push_back(edge{a[i]/*注意轉換*/,0});
}
}
//單點
e[a[v]+D].push_back(edge{a[u],w});
//單點->區間&區間->單點
connect(l,r,v,w,1,1,n,op%2);
//注意圖上節點向樹上節點編號轉換 i->a[i] 在dijkstra時以及答案統計和輸出時
trie
P5283 擺
P4551
void insert(int s){
int p=0;
for(int i=(1<<30);i>0;i>>=1){
bool t=s&i;//注意bool
if(!ch[p][t])ch[p][t]=++id;
p=ch[p][t];
}
cnt[p]++;
}
int ask(int s){
int p=0,ret=0;
for(int i=(1<<30);i>0;i>>=1){
bool t=s&i;
if(ch[p][!t]){
ret+=i;
p=ch[p][!t];
}else p=ch[p][t];
}
return ret;
}
//d[i]為樹上異或字首
insert(d[i]);
ans=max(ans,ask(d[i]));