分塊
暴力破解法
枚舉
折半
Meet-in-the-middle
給你 n 個數字 (n≤40) 和一個 x
請問有多少種取法讓總和等於 x
暴力枚舉
O(2n)
240≈1012 肯定TLE吧
n 超級小
先把序列切成左右兩半後暴力枚舉
再找有沒有可能有類似分治的方法將答案合併
此時枚舉左半,對右半做二分搜就好了
複雜度分析
-
枚舉左右兩半: O(n×22n)
-
合併:O(log 22n×22n)=O(n×22n)
-
總複雜度:O(n×22n)
1012 被砍到只剩 2×107 了
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";
}
n 維護
sqrt decomposition
看到這題你想到什麼?
資料結構(線段數、稀疏表、treap)....
最接近暴力解的解法?
切一切!
分塊的想法
4 | 2 | 1 |
---|
4 | 8 | 7 | 6 | 2 | 9 | 1 | 3 | 2 |
---|
以剛剛那題舉例,暴力查詢複雜度是O(n2)
假設每 k 數字個分成1塊,查詢時複雜度就是O(k+kn)
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)
完整包含該塊(最多kn塊)
- 修改懶標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,砸下算幾不等式
-
得 kn+k≥2k×kn
-
取 k=n
-
得最佳時間複雜度 O(n)
防破台題
這裡有n個洞,每個洞都有彈力強度ai,你把球丟進去之後會往右邊彈ai個洞,直到彈出去
共有 q 筆操作:
- 修改位置 x 的彈力強度 ax
- 詢問從第 x 位置開始彈,求彈出去之前的最後一個洞和從起點至終點總共彈了幾個洞
暴力解
先對每一格找出下一格
對於每一筆詢問直接模擬
O(q×n) TLE
防破台題怎麼可能這麼簡單
初始化:
-
把 k 個位置當作一塊,共 kn 塊
-
記錄下一個位置 nexti
-
如果下一個位置還在同一塊,那就記錄下下個位置,同時記錄最後走到的上一個位置 lasti 和移動的步數 stepi
複雜度: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 大的數來代替
更新:
-
更新這一格的彈力
-
將整塊的 nexti, lasti, stepi 照初始化的方式更新
複雜度:O(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
如果修改這一格,前一格也要一起修改
詢問:
照著nexti, lasti, stepi 模擬,同時記錄上一格和總步數
複雜度:O(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≤105, m≤3×105)
Solution A
Solution B
枚舉每一條邊,如果邊上的兩點都和這個點有邊,可以構成三角形
複雜度:O(M)
枚舉與該點相鄰的每個點,如果任兩個點有邊
可以構成三角形
複雜度:O(d2)
Solution A | Solution B | |
---|---|---|
O(M)
O(d2)
d<K
d≥K
分析總複雜度
O(K×∑di)
=O(MK)
最多K2M個點
O(KM2)
K 取 M 得最佳複雜度 O(MM)
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×q)
- 右界在換塊前只會走 n 步 O(n×kn)
- 得出 O(k×q+kn2),取k=qn
- 乘回 O(a),總複雜度得 O(a×n×q)
ZJ b417
- 給你一組序列
- 多筆詢問求區間眾數
- 記錄每個數字出現次數 cnti
- 再記錄 cnti 的數字出現幾次
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";
}
更多題目
分塊
By doubledown13
分塊
- 226