ACM競賽中數據結構題目心得:分塊【With HDU4366】


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM