我在ACM競賽中,一般負責決定隊伍的下限:水題能不能清理出來……其他太高深的題目,我表示我還是挺無腦的,一般都不老會的……只有數據結構類題還是挺得心應手的……而個人心得體會最深刻的還是無腦的方法:個人稱為根號N法……
主要思想就是將待操作的長度為N的區間分成大小為sqrt(N)的塊,然后實現各種操作……
一些常用定義:
MAGIC:定義一個塊的大小,如字面意思,一個莫名其妙的數字……
於是,我們把一段長度為N的區間,分成了若干長度為 MAGIC 的區間:[0,magic),[magic, 2magic)....
於是易得,i / MAGIC 就是點 i 所在塊的編號,若 i % MAGIC == 0,則證明由點 i 開始是一個新區間
一般來講,我們在預處理和修改的時候,維護兩個信息,一個是序列,另一個是塊
應用1:
靜態RMQ問題,求一個長度為N的序列中區間 l,r 中的最大/小值
在讀入序列的時候預處理得到每個塊里的最大值
對一段區間l,r進行查詢的時候,將其分成若干段 [l , magic * i) , [magic * i , magic * (i + 1)) ... [magic * j .. r],取最大值
其中左右兩端需要暴力,然后中間的 [magic * i , magic * (i + 1)) ... 等區間,直接調用預處理的結果
預處理O(N),每個查詢O(sqrt(N))
int num[11111];
int max[111];
int MAGIC = 111;
int n;
void init() {
for (int i = 0; i <n; i++) {
if (i % MAGIC == 0 || num[i] > max[i / MAGIC]) {
max[i / MAGIC] = num[i];
}
}
}
int query(int l,int r) {
int ret = num[l];
for (int j = l; j <= r;) {
if (j % MAGIC == 0 && j + MAGIC - 1 <= r) {
if (max[j / MAGIC] > ret) ret = max[j / MAGIC];
j += MAGIC;
} else {
if (num[j] > ret) ret = num[j];
j += 1;
}
}
return ret;
}
應用2:動態RMQ問題,在應用1的基礎上增加條件:可以修改某點的值
修正某點的值,然后維護該點所在的塊,復雜度O(sqrt(N))
void update(int x,int delta) {
num[x] = delta;
int l = x / MAGIC * MAGIC;
int r = l + MAGIC;
for (int i = l; i < r; i++) {
if (i % MAGIC == 0 || num[i] > max[i / MAGIC]) max[i / MAGIC] = num[i];
}
}
其他應用:區間求和(靜態,動態),區間染色,等等等等……To Be continued……如果題目時間卡的不是太緊,都可以用sqrt(N)大法水一水
精通線段樹的同志們應該更有心得,這個方法相當於一層分根號N叉的一個線段樹……似乎這個方法沒有什么意義,不過這個方法各種意義上都是更加無腦,思維復雜度,編碼復雜度都很低,而且隨着現在機器越來越好,根號N的方法很難被卡住,還是值得一試的……
下面看看今天多校的題目:http://acm.hdu.edu.cn/showproblem.php?pid=4366
題意是給一個樹,樹上每個節點都有兩個屬性:忠誠度和能力,給出若干查詢,求每個子樹中能力 > 樹根能力的點中,忠誠度最高的那個
首先容易想到DFS一趟,把問題轉化為區間查詢問題,相當於查找一段區間[L,R]里,能力 > X 的點中,忠誠度最高的點
於是決定用根號N法水一水:把區間分塊:[0,MAGIC), [MAGIC, 2MAGIC....),並按照塊內的節點能力值排序
然后應用個簡單DP思想,O(MAGIC) 推出從塊內每個點開始到塊末尾的最大忠誠度是多少,這樣一個塊的信息就初始化完成了
查詢的時候,如果待查詢區間[l,R]和塊相交,則直接暴力,如果[l,R]完全包含一個塊,則在塊里二分能力值X,然后返回塊內能力值 > X 的最大忠誠度
#include <cstdio>
#include <vector>
#include <map>
#include <algorithm>
using namespace std;
typedef long long Long;
const int MAGIC = 250;
struct staff {
int loyalty;
int ability;
};
bool operator < (staff a,staff b) {
return a.ability < b.ability;
}
vector<int> adj[55555];
staff arr[55555];
int pos[55555];
map<int,int> rev;
int tot;
staff list[55555];
staff sorted[55555];
int maxl[55555];
int size[55555];
int n,q;
int dfs(int now) {
pos[now] = tot;
list[tot] = sorted[tot] = arr[now];
tot ++;
int ret = 1;
for (int i = 0; i < adj[now].size(); i++) {
ret += dfs(adj[now][i]);
}
return size[pos[now]] = ret;
}
int work(int l,int r,int val) {
// 在塊l,r內返回能力值 > val 的最大忠誠
// 二分區間端點判定
if (sorted[r].ability <= val) return -1;
if (sorted[l].ability > val) return maxl[l];
while (l + 1 < r) {
int mid = (l + r) >> 1;
if (sorted[mid].ability > val) r = mid; else l = mid;
}
return maxl[r];
}
int main() {
int nn;
scanf("%d",&nn);
while (nn--) {
scanf("%d%d",&n,&q);
for (int i = 0; i < n; i++) {
adj[i].clear();
arr[i].loyalty = arr[i].ability = -1;
sorted[i] = list[i] = arr[i];
}
memset(maxl,0,sizeof(maxl));
memset(size,0,sizeof(size));
memset(pos,0,sizeof(pos));
rev.clear();
rev[-1] = -1;
// 以上是初始化
for (int i = 1; i < n; i++) {
int fa,l,a;
scanf("%d%d%d",&fa,&l,&a);
adj[fa].push_back(i);
// 由於保證忠誠度不同,為了操作方便,map忠誠度到人
rev[arr[i].loyalty = l] = i;
arr[i].ability = a;
}
tot = 0;
dfs(0);
// 以上是構圖DFS
for (int i = 0; i < n; i += MAGIC) {
int j = i + MAGIC;
if (j > n) break;
// 塊內排序
sort(sorted + i, sorted + j);
// DP構造忠誠度
maxl[j - 1] = sorted[j - 1].loyalty;
for (int k = j - 2; k >= i; k--) {
maxl[k] = maxl[k + 1] > sorted[k].loyalty ? maxl[k + 1] : sorted[k].loyalty;
}
}
while (q--) {
int st; scanf("%d",&st);
int val = arr[st].ability;
st = pos[st];
int ed = st + size[st] - 1;
int ans = -1;
for (int i = st; i <= ed;) {
// 二分塊
if (i % MAGIC == 0 && i + MAGIC - 1 <= ed) {
int tmp = work(i, i + MAGIC - 1, val);
if (tmp > ans) ans = tmp;
i += MAGIC;
} else {
// 暴力搞
if (list[i].ability > val && list[i].loyalty > ans) ans = list[i].loyalty;
i ++;
}
}
printf("%d\n",rev[ans]);
}1
}
return 0;
}
今天嘗到甜頭之后,試圖把POJ2104也根號N大法了
題意是給一個序列,查詢區間內的第K大值
我們同樣分塊,預處理,把塊內元素排序。然后對每個查詢,二分第K大值,設為X,對X,統計區間內有多少數小於X,如果區間包含塊則二分,否則暴力。
這樣復雜度為二分log(x) × max(塊數 × log(MAGIC) + MAGIC × 2),經無數次調換MAGIC,以及應用了WS讀入法,也過不了……
於是,咱們將分塊方法優化一下,也弄點層次出來:設第 i 層塊大小為 1 << i,初始化同理。
每次查詢的時候,試圖走最大的 2 的冪次的步長……
直接上代碼似乎更容易明白:
#include <stdio.h> #include <algorithm> using namespace std; const int MAGIC = 18; int n,m; int arr[111111]; int sorted[20][111111];
// 找出第ind層,區間為l,r的塊中有多少數 < val int work(int ind,int l,int r,int val) { int *sorted = ::sorted[ind]; if (sorted[l] >= val) return 0; if (sorted[r] < val) return r - l + 1; int st = l; while (l + 1 < r) { int mid = (l + r) >> 1; if (sorted[mid] < val) l = mid; else r = mid; } return r - st; } int main() { scanf("%d%d",&n,&m); for (int i = 0; i < n; i++) { scanf("%d",arr + i); } for (int j = 0; j < MAGIC; j++) { for (int i = 0; i < n; i++) { sorted[j][i] = arr[i]; } }
// 預處理每層大小為 2,4,8,16... 的塊 for (int j = 1; j < MAGIC; j++) { int step = 1 << j; for (int i = 0; i + step - 1 < n; i += step) { sort(sorted[j] + i, sorted[j] + i + step); } } while (m --) { int l,r,k; scanf("%d%d%d",&l,&r,&k); l --; r --; int ll = -1e9 - 1; int rr = 1e9 + 1; while (ll + 1 < rr) { int rank = 0; int mid = (ll + rr) >> 1; for (int i = l; i <= r;) { for (int j = MAGIC; j >= 0; j--) {
// 選擇最大的2的冪次的步長,調用塊里對應的信息 int step = 1 << j; if (i % step == 0 && i + step - 1 <= r) { rank += work(j,i, i + step - 1,mid); i += step; break; } } } if (rank < k) ll = mid; else rr = mid; } printf("%d\n",ll); } return 0; }
這個復雜度的話,外層二分,log(N),每次會分log(N)塊,塊內二分Log(N),總復雜度Log(N)^3
在一個好的Blog上見過句話:定義若干正則集合,並將他們組織成某種合適的結構,而查找算法就是要把查找的結果表示成若干個正則集合的划分,進而在每個正則集合中通過枚舉的方式實現查找。可見,分塊,線段樹等等都是這個思想
