分塊

暴力破解法

枚舉

折半

Meet-in-the-middle

給你 \(n\) 個數字 \((n\leq 40)\) 和一個 \(x\)

請問有多少種取法讓總和等於 \(x\)

暴力枚舉

\(O(2^n)\)

\(2^{40}\approx 10^{12}\) 肯定TLE

\(n\) 超級小

先把序列切成左右兩半後暴力枚舉

再找有沒有可能有類似分治的方法將答案合併

此時枚舉左半,對右半做二分搜就好了

複雜度分析

  • 枚舉左右兩半: \(O(n\times 2^{\frac{n}{2}})\)

  • 合併:\(O(log\ 2^{\frac{n}{2}} \times 2^{\frac{n}{2}})=O(n\times 2^{\frac{n}{2}})\)

  • 總複雜度:\(O(n\times 2^{\frac{n}{2}})\)

\(10^{12}\) 被砍到只剩 \(2\times 10^{7}\) 了

cOdE

#include<iostream>
#include<algorithm>
#include<vector>
#define int long long
using namespace std;
signed main(){
	int n,x,mid,t;
	vector<int> p,q,a,b;
	cin>>n>>x;
	int l=(n+1)/2,r=n-l;
	for(int i=0;i<l;i++){
		cin>>t;
		p.push_back(t);
	} 
	for(int i=l;i<n;i++){
		cin>>t;
		q.push_back(t);
	} 
	for(int i=0;i<(1<<l);i++){
		int k[21],cur=i,now=0;
		for(int j=0;j<l;j++){
			k[j]=cur%2;
			cur/=2;
		}
		for(int j=0;j<l;j++) now+=k[j]*p[j];
		a.push_back(now);
	}
	for(int i=0;i<(1<<r);i++){
		int k[21],cur=i,now=0;
		for(int j=0;j<r;j++){
			k[j]=cur%2;
			cur/=2;
		}
		for(int j=0;j<r;j++) now+=k[j]*q[j];
		b.push_back(now);
	}
	int ans=0;
	sort(b.begin(),b.end());
	for(int i=0;i<a.size();i++) ans+=(int)(upper_bound(b.begin(),b.end(),x-a[i])-lower_bound(b.begin(),b.end(),x-a[i]));
	cout<<ans<<"\n";
}

\(\sqrt{n}\) 維護

sqrt decomposition

看到這題你想到什麼?

資料結構(線段數、稀疏表、treap)....

最接近暴力解的解法?

切一切!

分塊的想法

4 2 1
4 8 7 6 2 9 1 3 2

以剛剛那題舉例,暴力查詢複雜度是\(O(n^2)\)

假設每 \(k\) 數字個分成1塊,查詢時複雜度就是\(O(k+\frac{n}{k})\)

\(min(8,7,2,1)\)=1

全部數字每\(K\)格切一塊

CoDe(中間點一下)

#include<iostream>
#include<cmath>
#define int long long
using namespace std;
const int N=1e5+30;
int a[N],sq1[350],sq2[350];  //10^2.5 =316.22
signed main(){
	int n,m,sq;
	cin>>n>>m;
	sq=sqrt(n);
	for(int i=0;i<350;i++){
		sq1[i]=-1;
		sq2[i]=1e15;
	}
	for(int i=0;i<n;i++){
		cin>>a[i];
		sq1[i/sq]=max(sq1[i/sq],a[i]);
		sq2[i/sq]=min(sq2[i/sq],a[i]);
	}
	int l,r;
	while(m--&&cin>>l>>r){
		l--,r--;
		int L=l/sq,R=r/sq,mn=1e15,mx=-1;
		if(L==R){
			for(int i=l;i<=r;i++){
				mx=max(mx,a[i]);
				mn=min(mn,a[i]);
			}
		}else{
			for(int i=L+1;i<R;i++){
				mx=max(mx,sq1[i]);
				mn=min(mn,sq2[i]);
			}
			for(int i=l;i<(L+1)*sq;i++){
				mx=max(mx,a[i]);
				mn=min(mn,a[i]);
			}
			for(int i=R*sq;i<=r;i++){
				mx=max(mx,a[i]);
				mn=min(mn,a[i]);
			}
		}
		cout<<mx-mn<<"\n";
	}
} 

那動態修改呢?

CSES 1649

單點修改

4 1
4 8 7 6 9 1 3 2
  • 修改一個值 \(O(1)\)
  • 暴搜該塊 \(O(k)\)
  • 複雜度 \(O(k)\)

100

6

2

2

區間修改

8
4 8 7 6 100 9 3 2

就算用了分塊,你還是逃不過懶標

tag:200

200

2

200

不完整包含該塊(左右最多2塊)

  • 懶標向下推 \(O(k)\)
  • 改值 \(O(k)\)
  • 複雜度 \(O(k)\)

完整包含該塊(最多\(\frac{n}{k}\)塊)

  • 修改懶標\(O(1)\)
  • 複雜度 \(O(1)\)

6

1

1

cOde(單點修改,區間查詢)

#include<iostream>
#include<cmath>
#define int long long
using namespace std;
const int N=2e5+30;
int n,q,k,a[N],sq[460];  // sqrt(2*10^5) =447.213
void upd(int pos,int val){
	int p=pos/k;
	a[pos]=val,sq[p]=1e9+7;
	for(int i=p*k;i<(p+1)*k;i++) sq[p]=min(sq[p],a[i]);
}
int que(int l,int r){
	int ans=1e9+7,L=l/k,R=r/k;
	if(L==R){
		for(int i=l;i<=r;i++) ans=min(ans,a[i]);
	}else{
		for(int i=L+1;i<R;i++) ans=min(ans,sq[i]);
		for(int i=l;i<(L+1)*k;i++) ans=min(ans,a[i]);
		for(int i=R*k;i<=r;i++) ans=min(ans,a[i]);
	}
	return ans;
}
signed main(){
	cin>>n>>q;
	k=sqrt(n);
	for(int i=0;i<455;i++) sq[i]=1e9+7;
	for(int i=0;i<n;i++){
		cin>>a[i];
		sq[i/k]=min(sq[i/k],a[i]);
	}
	int x,y,z;
	while(q--&&cin>>x>>y>>z){
		if(x==1) upd(y-1,z);
		else cout<<que(y-1,z-1)<<"\n";
	}
} 

CoDE(區間修改,區間查詢)

#include<iostream>
#include<cmath>
#define int long long
using namespace std;
const int N=2e5+30;
int n,q,k,a[N],sq[460],lazy[460];  // sqrt(2*10^5) =447.213
void upd(int l,int r,int val){
	int L=l/k,R=r/k;
	if(L==R){
		sq[L]=1e9+7;
		for(int i=L*k;i<(L+1)*k;i++){
			if(i>=l&&i<=r) a[i]=val;
			else if(lazy[L]!=-1) a[i]=lazy[L];
			sq[L]=min(sq[L],a[i]);
		}
		lazy[L]=-1;
	}else{
		for(int i=L+1;i<R;i++) sq[i]=lazy[i]=val;
		for(int i=L*k;i<(L+1)*k;i++){
			if(i>=l) a[i]=val;
			else if(lazy[i]!=-1) a[i]=lazy[L];
			sq[L]=min(sq[L],a[i]);
		}
		for(int i=R*k;i<min((R+1)*k,n);i++){
			if(i<=r) a[i]=val;
			else if(lazy[R]!=-1) a[i]=lazy[R];
			sq[R]=min(sq[R],a[i]);
		}
		lazy[L]=lazy[R]=-1;
	}
}
int que(int l,int r){
	int ans=1e9+7,L=l/k,R=r/k;
	if(L==R){
		if(lazy[L]!=-1) ans=sq[L];
		else for(int i=l;i<=r;i++) ans=min(ans,a[i]);
	}else{
		for(int i=L+1;i<R;i++) ans=min(ans,sq[i]);
		if(lazy[L]!=-1) ans=min(ans,sq[L]);
		else for(int i=l;i<(L+1)*k;i++) ans=min(ans,a[i]);
		if(lazy[R]!=-1) ans=min(ans,sq[R]);
		else for(int i=R*k;i<=r;i++) ans=min(ans,a[i]);
	}
	if(ans==-1) exit(0);
	return ans;
}
signed main(){
	cin>>n>>q;
	k=sqrt(n);
	for(int i=0;i<455;i++) sq[i]=1e9+7,lazy[i]=-1;
	for(int i=0;i<n;i++){
		cin>>a[i];
		sq[i/k]=min(sq[i/k],a[i]);
	}
	int x,y,z,val;
	while(q--&&cin>>x>>y>>z){
		if(x==1) upd(y-1,y-1,z);
		else cout<<que(y-1,z-1)<<"\n";
	}
} 

壓完行數發現我的線段樹行數比較少 XD

  • 為了決定 \(k\),砸下算幾不等式

  • 得 \(\frac{n}{k}+k\geq 2 \sqrt {k\times \frac{n}{k}}\)

  • 取 \(k=\sqrt{n}\)

  • 得最佳時間複雜度 \(O(\sqrt{n})\)

防破台題

這裡有\(n\)個洞,每個洞都有彈力強度\(a_i\),你把球丟進去之後會往右邊彈\(a_i\)個洞,直到彈出去

共有 \(q\) 筆操作:

  1. 修改位置 \(x\) 的彈力強度 \(a_x\) 
  2. 詢問從第 \(x\) 位置開始彈,求彈出去之前的最後一個洞和從起點至終點總共彈了幾個洞

暴力解

先對每一格找出下一格

對於每一筆詢問直接模擬

\(O(q\times n)\) TLE

防破台題怎麼可能這麼簡單

 初始化:

  1. 把 \(k\) 個位置當作一塊,共 \(\frac{n}{k}\) 塊

  2. 記錄下一個位置 \(next_i\)

  3. 如果下一個位置還在同一塊,那就記錄下下個位置,同時記錄最後走到的上一個位置 \(last_i\) 和移動的步數 \(step_i\)

  4. 複雜度:\(O(n)\)

倒著做 !

a 1 1 1 1 1 2 8 2
next 2 3 4 5 6 8 X X
next 3 3 5 5 8 8 X X

以範測舉例 \(n=8, k=2\)

last 2 2 4 4 6 6 7 8
step 2 1 2 1 2 1 1 1

X 代表飛出去了,可以設一個比 \(n\) 大的數來代替

 更新:

  1. 更新這一格的彈力

  2. 將整塊的 \(next_i\), \(last_i\), \(step_i\) 照初始化的方式更新

  3. 複雜度:\(O(\sqrt{n})\)

a 1 1 1 1 1 2 8 2
next 3 3 5 5 8 8 X X
last 2 2 4 4 6 6 7 8
step 2 1 2 1 2 1 1 1

如果其中一塊被改動了,同一塊在它左邊的位置也要同時改變

3

4

1

1

如果修改這一格,前一格也要一起修改

 詢問:

照著\(next_i\), \(last_i\), \(step_i\) 模擬,同時記錄上一格和總步數

複雜度:\(O(\sqrt{n})\)

a 3 1 1 1 1 2 8 2
next 4 3 5 5 8 8 X X
last 1 2 4 4 6 6 7 8
step 1 1 2 1 2 1 1 1

最後抵達: 8

總步數:\(1+1+2+1=5\)

(假設初始位置為1)

cODe

#include<iostream>
#include<cmath>
using namespace std;
typedef pair<int,int> p;
const int N=1e5+30;
int n,m,r,k,power[N],nxt[N],stp[N],lst[N];
void find1(int cur){
	int q=cur/k;
	for(int i=cur;i>=max(q*k,1);i--){
		int f=power[i]+i;
		if(f>n) nxt[i]=n+1,stp[i]=1,lst[i]=i;
		else if(f>=(q+1)*k) nxt[i]=f,stp[i]=1,lst[i]=i;
		else nxt[i]=nxt[f],stp[i]=stp[f]+1,lst[i]=lst[f];
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n>>m;
	k=sqrt(n);r=(n/k)+1;
	for(int i=1;i<=n;i++) cin>>power[i];
	for(int i=r;i>0;i--) find1(min(n,i*k-1));
	while(m--){
		int k,a,b;
		cin>>k>>a;
		if(!k){
			cin>>b;
			power[a]=b;
			find1(a);
		}else{
			int now=a,ans1=a,ans2=0;
			while(now<=n) ans2+=stp[now],ans1=lst[now],now=nxt[now];
			cout<<ans1<<" "<<ans2<<"\n";
		}
	}
} 

測資分塊

Solution A Solution B
Case 1
Case 2

TLE

AC

AC

TLE

假設你在寫題目時想到2個方法

結果...

那就...

if(Case1) SolutionA();
else SolutionB();

AC

有 \(n\) 個點和 \(m\) 條邊

假設你能 \(O(1)\) 找出 \((i,j)\) 有邊

問可以組成幾個三角形

\((n\leq 10^5,\ m\leq 3 \times 10^5)\)

Solution A

Solution B

枚舉每一條邊,如果邊上的兩點都和這個點有邊,可以構成三角形

複雜度:\(O(M)\)

枚舉與該點相鄰的每個點,如果任兩個點有邊

可以構成三角形

複雜度:\(O(d^2)\)

Solution A  Solution B   

\(O(M)\)

\(O(d^2)\)

\(d < K\)

\(d \geq K\)

分析總複雜度

\(O(K\times\sum d_i)\)

\(=O(MK)\)

最多\(\frac{2M}{K}\)個點

\(O(\frac{M^2}{K})\)

\(K\) 取 \(\sqrt{M}\) 得最佳複雜度 \(O(M\sqrt{M})\)

AC

莫隊算法

Mo's algorithm

離線做分塊

你有 \(Q\) 筆詢問

假設你有 \([L,R]\) 的答案

你可以 \(O(a)\) 求得\([L-1,R]\)、\([L+1,R]\)、\([L,R-1]\)、\([L,R+1]\)

(就是左右界向外推或向內縮一格的答案)

\(O(a)\) 通常不能太大,通常是 \(O(1)\) 或 \(O(log\ n)\)

  • 將整個序列每 \(K\) 格分一塊(假設分3塊)
  • 每筆詢問先以左界分塊排序
  • 再以右界位置排序

複雜度分析

  • 左界只會在塊內跳動 \(O(k\times q)\)
  • 右界在換塊前只會走 \(n\) 步 \(O(n\times \frac{n}{k})\)
  • 得出 \(O(k\times q + \frac{n^2}{k})\),取\(k=\frac{n}{\sqrt{q}}\)
  • 乘回 \(O(a)\),總複雜度得 \(O(a\times n\times \sqrt{q})\)

ZJ b417

  • 給你一組序列
  • 多筆詢問求區間眾數
  • 記錄每個數字出現次數 \(cnt_i\)
  • 再記錄 \(cnt_i\) 的數字出現幾次

COde

#include<iostream>
#include<algorithm>
#include<cmath>
#define p1 first
#define p2 second
using namespace std;
typedef pair<int,int> p;
const int N=1e5+30,M=N*10;
int s[N],a[N],b[N],mx=0; // a:數字出現的次數 b: a[i]的出現次數 
pair<int,p> ans[M];
struct Q{
	int sq,r,l,num;
}que[M];
bool cmp(Q x,Q y){
	if(x.sq!=y.sq) return x.sq<y.sq;
	return x.r<y.r;
}
void add(int x){
	a[s[x]]++;
	b[a[s[x]]]++;
	b[a[s[x]]-1]--;
	if(mx==a[s[x]]-1) mx=a[s[x]];
}
void sub(int x){
	a[s[x]]--;
	b[a[s[x]]]++;
	b[a[s[x]]+1]--;
	if(mx==a[s[x]]+1&&b[a[s[x]]+1]==0) mx=a[s[x]];
}
int main(){
	int n,m,k;
	cin>>n>>m;
	k=n/sqrt(m);
	for(int i=0;i<n;i++) cin>>s[i];
	for(int i=0;i<m;i++){
		cin>>que[i].l>>que[i].r;
		que[i].l--,que[i].r--;
		que[i].sq=que[i].l/k,que[i].num=i;
	}
	sort(que,que+m,cmp);
	for(int i=0;i<=n+5;i++) a[i]=b[i]=0;
	b[0]=n;
	int L=que[0].l,R=que[0].r;
	for(int i=que[0].l;i<=que[0].r;i++) add(i);
	ans[0]={que[0].num,{mx,b[mx]}};
	for(int i=1;i<m;i++){
		while(R<que[i].r) add(++R);
		while(L>que[i].l) add(--L);
		while(R>que[i].r) sub(R--);
		while(L<que[i].l) sub(L++);
		ans[i]={que[i].num,{mx,b[mx]}};
	} 
	sort(ans,ans+m);
	for(int i=0;i<m;i++) cout<<ans[i].p2.p1<<" "<<ans[i].p2.p2<<"\n";
}

更多題目

Made with Slides.com