ACM竞赛中数据结构题目心得:分块【With HDU4366】

http://www.cnblogs.com/sweetsc/archive/2012/08/15/2639395.html

我在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 的最大忠诚度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#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) {<br> // 在块l,r内返回能力值 > val 的最大忠诚<br>// 二分区间端点判定
     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;<br> // 以上是初始化
         for  ( int  i = 1; i < n; i++) {
             int  fa,l,a;
             scanf ( "%d%d%d" ,&fa,&l,&a);
             adj[fa].push_back(i);<br> // 由于保证忠诚度不同,为了操作方便,map忠诚度到人
             rev[arr[i].loyalty = l] = i;
             arr[i].ability = a;
         }
         tot = 0;
         dfs(0);<br> // 以上是构图DFS
         for  ( int  i = 0; i < n; i += MAGIC) {
             int  j = i + MAGIC;
             if  (j > n)  break ;<br> // 块内排序
             sort(sorted + i, sorted + j);<br> // 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;) {<br> // 二分块
                 if  (i % MAGIC == 0 && i + MAGIC - 1 <= ed) {
                     int  tmp = work(i, i + MAGIC - 1, val);
                     if  (tmp > ans) ans = tmp;
                     i += MAGIC;
                 else  {<br> // 暴力搞
                     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上见过句话:定义若干正则集合,并将他们组织成某种合适的结构,而查找算法就是要把查找的结果表示成若干个正则集合的划分,进而在每个正则集合中通过枚举的方式实现查找。可见,分块,线段树等等都是这个思想

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值