我在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上見過句話:定義若干正則集合,並將他們組織成某種合適的結構,而查找算法就是要把查找的結果表示成若干個正則集合的划分,進而在每個正則集合中通過枚舉的方式實現查找。可見,分塊,線段樹等等都是這個思想