*说明:本文所有文章来自http://www.notonlysuccess.com/index.php/acm_algorithm_list/*
#第k元素log(n)算法–划分树
前几天学线段树,这个经典的K-th number一直没有做,关键是听别人说复杂度是log(n)^3,我对这个需要两次二分+一次查找的算法非常的不爽,于是一直拖着没搞
今天正准备着手这题的时候,发现PKU的Disscuss有人提到log(n)的算法,而且编程复杂度比log(n)^3的还小,于是对这种算法充满了憧憬,那个log(n)^3的写到一半也放弃了(其实log(n)^3的归并树算法化简了之后就是求n个有序数列的第k大数)
YY了很久之后,得到下边这个代码..关键部分已经很明白的加了的注释
完全看明白之后会发现一个非常有趣的现象,划分树逆着做就变成了归并树
(其实我也不知道这是不是hyerty大神所说的划分树,乱YY的)
画了一颗划分树对数列[1 5 2 3 6 4 7 3 0 0]进行划分,下图有助于理解(红色表示该数被分到左儿子)
#define M 100001
struct Seg_Tree{
int left,right;
int mid() {
return (left + right) >> 1;
}
}tt[M*4];
int len;
int sorted[M];
int toLeft[20][M];
int val[20][M];
void build(int l,int r,int d,int idx) {
tt[idx].left = l;
tt[idx].right = r;
if(tt[idx].left == tt[idx].right) return ;
int mid = tt[idx].mid();
int lsame = mid - l + 1;//lsame表示和val_mid相等且分到左边的
for(int i = l ; i <= r ; i ++) {
if(val[d][i] < sorted[mid]) {
lsame --;//先假设左边的数(mid - l + 1)个都等于val_mid,然后把实际上小于val_mid的减去
}
}
int lpos = l;
int rpos = mid+1;
int same = 0;
for(int i = l ; i <= r ; i ++) {
if(i == l) {
toLeft[d][i] = 0;//toLeft[i]表示[ tt[idx].left , i ]区域里有多少个数分到左边
} else {
toLeft[d][i] = toLeft[d][i-1];
}
if(val[d][i] < sorted[mid]) {
toLeft[d][i] ++;
val[d+1][lpos++] = val[d][i];
} else if(val[d][i] > sorted[mid]) {
val[d+1][rpos++] = val[d][i];
} else {
if(same < lsame) {//有lsame的数是分到左边的
same ++;
toLeft[d][i] ++;
val[d+1][lpos++] = val[d][i];
} else {
val[d+1][rpos++] = val[d][i];
}
}
}
build(l,mid,d+1,LL(idx));
build(mid+1,r,d+1,RR(idx));
}
int query(int l,int r,int k,int d,int idx) {
if(l == r) {
return val[d][l];
}
int s;//s表示[ l , r ]有多少个分到左边
int ss;//ss表示 [tt[idx].left , l-1 ]有多少个分到左边
if(l == tt[idx].left) {
s = toLeft[d][r];
ss = 0;
} else {
s = toLeft[d][r] - toLeft[d][l-1];
ss = toLeft[d][l-1];
}
if(s >= k) {//有多于k个分到左边,显然去左儿子区间找第k个
int newl = tt[idx].left + ss;
int newr = tt[idx].left + ss + s - 1;//计算出新的映射区间
return query(newl,newr,k,d+1,LL(idx));
} else {
int mid = tt[idx].mid();
int bb = l - tt[idx].left - ss;//bb表示 [tt[idx].left , l-1 ]有多少个分到右边
int b = r - l + 1 - s;//b表示 [l , r]有多少个分到右边
int newl = mid + bb + 1;
int newr = mid + bb + b;
return query(newl,newr,k-s,d+1,RR(idx));
}
}
int main() {
int T;
scanf("%d",&T);
while(T --) {
int n , m;
scanf("%d%d",&n,&m);
FOR(i,1,n+1) {
scanf("%d",&val[0][i]);
sorted[i] = val[0][i];
}
sort(sorted + 1 , sorted + n + 1);
build(1,n,0,1);
while(m --) {
int l,r,k;
scanf("%d%d%d",&l,&r,&k);
printf("%d\n",query(l,r,k,0,1));
}
}
return 0;
}
##后记:
写完后去PKU交了一下,原以为不是rank1也至少是前十,结果连1s都没跑进去…
看了数据后发现m<=5000很少
意味着 nlogn(建树常数较大) + mlngn
和 nlogn(建树常数小)+mlogn^3
前者没占多少优势...
在我们hduoj也找了一道,所幸这题m <= 100000很大
两题比较:
OJ nlog(n) + mlog(n) nlog(n) + mlog(n)^3
HDU 少于500MS 3000MS左右
PKU 1000MS左右 1000-2000MS
##2010.7.23跟新
扩展:
Minimum Sum
找到区间中的中位数,然后确定绝对值只和
就是找区间[l,r]的第(l-r+2)/2个数,而求和的话在Query函数里找到kth number后递归上来后再处理一下,需要另开一个数组sum[deep][i]表示第deep层,区间[ tt[idx].Left , i]的和
##我比较懒都没有解释什么,只写了个代码
这篇文章解释的很清楚,可以去看看
我的代码只是为了理解方便点写的这么麻烦,其实划分树可以写的很简洁很简洁的,有好多地方可以优化~~
#RMQ and LCA
RMQ (Range Minimum/Maximum Query) and LCA(Lowest Common Ancestor)
学后缀数组的时候遇到这个问题,不会,特地去找了资料…
发现农夫三拳的一篇强大翻译阐述了LCA和RMQ的关系,还有郭华阳 《RMQ与LCA问题》
区间最值问题,强大就强大在O(1)的询问
寻思着kth number能否由这个变形得到?
其他也没啥好探讨的…
献上模板和题目
##3264 Balanced Lineup 一维RMQ
#define M 50001
int val[M];
int Max[20][M];
int Min[20][M];
int idx[M];
void initRMQ(int n) {
idx[0] = -1;
for(int i = 1; i <= n ; i ++) {
idx[i] = (i&(i-1)) ? idx[i-1] : idx[i-1] + 1;
Min[0][i] = Max[0][i] = val[i];
}
for(int i = 1; i <= idx[n] ; i ++) {
int limit = n + 1 - (1<<i);
for(int j = 1; j <= limit ; j ++) {
Min[i][j] = min(Min[i-1][j] , Min[i-1][j+(1<<i>>1)]);
Max[i][j] = max(Max[i-1][j] , Max[i-1][j+(1<<i>>1)]);
}
}
}
int getval(int a,int b) {
int t = idx[b-a+1];
b -= (1<<t) - 1;
return max(Max[t][a] , Max[t][b]) - min(Min[t][a] , Min[t][b]);//返回最大值减最小值
}
int main() {
int n , m;
scanf("%d%d",&n,&m);
for(int i = 1; i <= n ; i ++) {//下标要从1开始
SS(val[i]);
}
initRMQ(n);
while(m --) {
int a , b ;
scanf("%d%d",&a,&b);
printf("%d\n",getval(a,b));
}
return 0;
}
##hdu2888 Check Corners 二维RMQ
//复杂度n*m*log(n)*log(m)
#define M 301
int val[M][M];
int Max[9][9][M][M];
int idx[M];
void initRMQ(int n , int m) {
for(int i = 1; i <= n ; i ++) {
for(int j = 1 ; j <= m ; j ++) {
Max[0][0][i][j] = val[i][j];
}
}
for(int i = 0; i <= idx[n] ; i ++) {
int limit1 = n + 1 - (1<<i);
for(int j = 0 ; j <= idx[m] ; j ++) {
if(!i && !j) continue;
int limit2 = m + 1 - (1<<j);
for(int ii = 1; ii <= limit1 ; ii ++) {
for(int jj = 1; jj <= limit2 ; jj ++) {
if(i) Max[i][j][ii][jj] = max( Max[i-1][j][ii+(1<<i>>1)][jj] , Max[i-1][j][ii][jj] );
else Max[i][j][ii][jj] = max( Max[i][j-1][ii][jj] , Max[i][j-1][ii][jj+(1<<j>>1)] );
}
}
}
}
}
int query(int a,int b,int c,int d) {
int n = idx[c-a+1], m = idx[d-b+1];
c -= (1<<n) - 1;d -= (1<<m) - 1;
return max(max(Max[n][m][a][b],Max[n][m][a][d]),max(Max[n][m][c][b],Max[n][m][c][d]));
}
int main() {
idx[0] = -1;
for(int i = 1; i <= 300 ; i ++) {
idx[i] = (i&(i-1)) ? idx[i-1] : idx[i-1] + 1;//放在外边计算
}
int n , m , Q;
while(~scanf("%d%d",&n,&m)) {
for(int i = 1; i <= n ; i ++) {
for(int j = 1; j <= m ; j ++) {
SS(val[i][j]);
}
}
initRMQ(n,m);
SS(Q);
while(Q --) {
int a,b,c,d;
scanf("%d%d%d%d",&a,&b,&c,&d);
if(a > c) swap(a,c);
if(b > d) swap(b,d);
int key = query(a,b,c,d);
printf("%d ",key);
if(key == val[a][b] || key == val[a][d] || key == val[c][b] || key == val[c][d]) {
puts("yes");
} else {
puts("no");
}
}
}
}
#后缀数组
今天拜读了罗穗骞的神作《后缀数组——处理字符串的有力工具》
这篇主要是模板以及应用,还有一篇许智磊的《后缀数组》主要讲概念和证明
两者结合疗效好- -
刚刚开始做的时候觉得不过尔尔,模板题而已
但越做到后边PKU3693越发觉有趣,非常巧妙的统计,随便把RMQ也学了
跟着论文做了几道题目,但还不知道以后出现后缀数组的题能不能解出来,呵呵
刷题途中发现3xian总是排第一—-无限崇拜并馋涎其模板,发现他的最大流,(kth number)图灵树,后缀树模板极其的高效
献上模板,倍增算法nlog(n),基本上时仿照论文里写的,加了点常数优化
另外一个DC3编程复杂度大,并且效率只是提高一点点,不是严格意义上的线性算法,没写(其实是我偷懒- -)
//rank从0开始
//sa从1开始,因为最后一个字符(最小的)排在第0位
//high从2开始,因为表示的是sa[i-1]和sa[i]
#define M 220000
int rank[M],sa[M],X[M],Y[M],high[M],init[M];
int buc[M];
void calhigh(int n) {
int i , j , k = 0;
for(i = 1 ; i <= n ; i++) rank[sa[i]] = i;
for(i = 0 ; i < n ; high[rank[i++]] = k)
for(k?k--:0 , j = sa[rank[i]-1] ; init[i+k] == init[j+k] ; k++);
}
bool cmp(int *r,int a,int b,int l) {
return (r[a] == r[b] && r[a+l] == r[b+l]);
}
void suffix(int n,int m = 128) {
int i , l , p , *x = X , *y = Y;
for(i = 0 ; i < m ; i ++) buc[i] = 0;
for(i = 0 ; i < n ; i ++) buc[ x[i] = init[i] ] ++;
for(i = 1 ; i < m ; i ++) buc[i] += buc[i-1];
for(i = n - 1; i >= 0 ; i --) sa[ --buc[ x[i] ]] = i;
for(l = 1,p = 1 ; p < n ; m = p , l *= 2) {
p = 0;
for(i = n-l ; i < n ; i ++) y[p++] = i;
for(i = 0 ; i < n ; i ++) if(sa[i] >= l) y[p++] = sa[i] - l;
for(i = 0 ; i < m ; i ++) buc[i] = 0;
for(i = 0 ; i < n ; i ++) buc[ x[y[i]] ] ++;
for(i = 1 ; i < m ; i ++) buc[i] += buc[i-1];
for(i = n - 1; i >= 0 ; i --) sa[ --buc[ x[y[i]] ] ] = y[i];
for(swap(x,y) , x[sa[0]] = 0 , i = 1 , p = 1 ; i < n ; i ++)
x[ sa[i] ] = cmp(y,sa[i-1],sa[i],l) ? p-1 : p++;
}
calhigh(n-1);//后缀数组关键是求出high,所以求sa的时候顺便把rank和high求出来
}
//当需要反复询问两个后缀的最长公共前缀时用到RMQ
int Log[M];
int best[20][M];
void initRMQ(int n) {//初始化RMQ
for(int i = 1; i <= n ; i ++) best[0][i] = high[i];
for(int i = 1; i <= Log[n] ; i ++) {
int limit = n - (1<<i) + 1;
for(int j = 1; j <= limit ; j ++) {
best[i][j] = min(best[i-1][j] , best[i-1][j+(1<<i>>1)]);
}
}
}
int lcp(int a,int b) {//询问a,b后缀的最长公共前缀
a = rank[a]; b = rank[b];
if(a > b) swap(a,b);
a ++;
int t = Log[b - a + 1];
return min(best[t][a] , best[t][b - (1<<t) + 1]);
}
int main() {
//预处理每个数字的Log值,常数优化,用于RMQ
Log[0] = -1;
for(int i = 1; i <= M ; i ++) {
Log[i] = (i&(i-1)) ? Log[i-1] : Log[i-1] + 1 ;
}
//*******************************************
// n为数组长度,下标0开始
// 将初始数据,保存在init里,并且保证每个数字都比0大
// m = max{ init[i] } + 1
// 一般情况下大多是字符操作,所以128足够了
//*******************************************
init[n] = 0;
suffix(n+1,m);
initRMQ(n);
}
下边收入几道论文上没提的后缀数组题目
zoj3199 Longest Repeated Substring
连续重复至少1次的最长串
spoj1811 Longest Common Substring
普通的LCS,同PKU2774,不过数据量超大,上边的模板超时,DC3刚好卡过,据说正解是后缀自动机,Orz
hdu2890 Longest Repeated subsequence
不可重叠k次最长重复子串.分层后排序贪心nlog(n)
#AC自动机
http://www.cs.uku.fi/~kilpelai/BSA05/lectures/slides04.pdf
http://www.docin.com/p-46845432.html(上边原文地址如果无法访问的话可以访问这一个~)
##这是我找到的AC自动机最好的资料
感觉网上其他一些资料都没能阐述的很好,只是草草的介绍一下fail指针,画几张草图而已,贴一下不是很搞笑甚至还带有错误的模板,真正精髓的地方都很模糊的一笔带过
这片资料虽然是英文的,阅读起来也许有一点点小障碍,而且不像其他一样长篇大论,只是精短的几句描述,但看完之后会有“大彻大悟”的感觉~好了,自己去欣赏这篇极品论文吧..
##下边给出几道题目以及解题思路供大家练习
模板题:
###hdoj2222 Keywords Search
网络流上流传最广的AC自动机模板题,问你目标串中出现了几个模式串
如果一个结点是单词末尾的话out标记为true,在search的时候对于每个结点都向fail指针找,找到out为true的就将其标记为false,且ans++
###hdoj2896 病毒侵袭
问你目标串中出现了几个模式串
同上一题,只是这题要输出模式串的ID,且字符是所有可见字符,要开[128]的儿子结点,还好没有太卡内存
###hdoj3065 病毒侵袭持续中
目标串中各个模式串出现了几次
这就更简单了,都不用把out标记成false了~
###zoj3430 Detect the Virus
解码之后就和模板题没什么差别
AC自动机+矩阵
###poj2778 DNA Sequence
问你长度为N的串中不包含了模式串的串有几个
n属于1 ~ 2000000000看到这个数据范围我们就应该敏感的想到这是矩阵~
最多100个结点,先建好所有结点(不包括模式串结尾的和fail指向结尾的结点,所以其实最多只有90个有效结点)之间的转化关系,然后二分矩阵乘法,复杂度O(100^3*log(2000000000))
###hdoj2243 考 研路茫茫——单词情结
问你长度为1~N的串中包含了模式串的串总共有几个
上题的加强版,先要把总数26^1 + 26^2 + … + 26^N算出来,然后减去所有不包含的…反正比上题恶心一点点
答案要模2^64,直接用unsinged __int64 就OK了
AC自动机+DP
###poj1625 Censored!
大数+简单DP
###hdoj2825 Wireless Password
位压缩,每个节点有2^10次状态,找到至少K个
###hdoj2296 Ring
每个单词有val,关键需要输出路径,比较挺麻烦的,需要倒过来,如果暴力点就直接用string吧。。
###hdoj2457 DNA REPAIR
就是不要走到尾结点上,如果和匹配串不相同的话+1dp
###zoj3228 Searching the String
找可以重叠和不能重叠的串个有多少,记录每个单词最后出现的pos,注意有重复的串
###Lost’s revenge
RE的神题,非常卡内存。。。空间复杂度(500*11^4)(这还是缩水后的,原来的是(2500*11^4)),时间复杂度要再乘上转化复杂度(4),如果状态变化的系数高一点就过不了。。表示要非常优化的代码才能过
http://zerojudge.tw/ShowProblem?problemid=b179 空罐 Cans
高中生题,做了一个小时,关键是debug一个白痴错误de了半个小时。惭愧啊。。不过这题比较好玩的是罐子基因分裂后第一个字符会消失,需要用fail指针来转移状态了
fail用来转移变短的状态,chd来转移变长的基因,同时还要记录基因的长度(长度l转移的时候需分三类讨论,l = 1,l 恰为根结点到当前结点的距离,l 大于该距离),DP状态是100*1500[长度,结点]用滚动数组
###hdoj3247 Resource Archiver
乳鸽的神题,状态很容易看出来,有50000*1024,很难保持,我用散列表超时了,用bitset刚好可以卡过,不过后来我想,只有尾结点才有效,中间的很多结点完全可以忽略,可以先用最短路吧各个尾结点之间的距离算出来,经过测试,不到50个点,马上就优化到50*1024了,本来9s多过的优化到了100多MS
###zoj3494 BCD Code
当年好像就是借着这题省赛捧杯的,AC自动机+数位DP,好题!
最后献上我的模板
update at 2012/6/6
/*
HDU2825 Wireless Password
*/
#include <cstdio>
#include <cstdlib>
#include <string>
#include <climits>
#include <iostream>
#include <vector>
#include <set>
#include <cmath>
#include <cctype>
#include <algorithm>
#include <sstream>
#include <map>
#include <cstring>
#include <queue>
using namespace std;
//MAX_NODE = StringNumber * StringLength
const int MAX_NODE = 101;
//节点个数,一般字符形式的题26个
const int CHILD_NUM = 26;
//特定题目需要
const int mod = 20090717;
class ACAutomaton {
private:
//每个节点的儿子,即当前节点的状态转移
int chd[MAX_NODE][CHILD_NUM];
//记录题目给的关键数据
int val[MAX_NODE];
//传说中的fail指针
int fail[MAX_NODE];
//队列,用于广度优先计算fail指针
int Q[MAX_NODE];
//字母对应的ID
int ID[128];
//已使用节点个数
int sz;
//特定题目需要
int dp[2][MAX_NODE][1<<10];
public:
//初始化,计算字母对应的儿子ID,如:'a'->0 ... 'z'->25
void Initialize() {
fail[0] = 0;
for (int i = 0 ; i < CHILD_NUM ; i ++) {
ID[i+'a'] = i;
}
}
//重新建树需先Reset
void Reset() {
memset(chd[0] , 0 , sizeof(chd[0]));
sz = 1;
}
//将权值为key的字符串a插入到trie中
void Insert(char *a,int key) {
int p = 0;
for ( ; *a ; a ++) {
int c = ID[*a];
if (!chd[p][c]) {
memset(chd[sz] , 0 , sizeof(chd[sz]));
val[sz] = 0;
chd[p][c] = sz ++;
}
p = chd[p][c];
}
val[p] = key;
}
//建立AC自动机,确定每个节点的权值以及状态转移
void Construct() {
int *s = Q , *e = Q;
for (int i = 0 ; i < CHILD_NUM ; i ++) {
if (chd[0][i]) {
fail[ chd[0][i] ] = 0;
*e ++ = chd[0][i];
}
}
while (s != e) {
int u = *s++;
for (int i = 0 ; i < CHILD_NUM ; i ++) {
int &v = chd[u][i];
if (v) {
*e ++ = v;
fail[v] = chd[ fail[u] ][i];
//以下一行代码要根据题目所给val的含义来写
val[v] |= val[ fail[v] ];
} else {
v = chd[ fail[u] ][i];
}
}
}
}
//解题,特定题目需要
int Work(int n,int m,int k) {
memset(dp[0] , 0 , sizeof(dp[0]));
dp[0][0][0] = 1;
int S = 0;
while (n --) {
int T = 1 - S;
memset(dp[T] , 0 , sizeof(dp[T]));
for (int i = 0 ; i < sz ; i ++) {
for (int j = 0 ; j < m ; j ++) {
if (dp[S][i][j]) {
for (int k = 0 ; k < CHILD_NUM ; k ++) {
int p = chd[i][k];
int st = (j | val[p]);
dp[T][p][st] += dp[S][i][j];
if (dp[T][p][st] >= mod) {
dp[T][p][st] -= mod;
}
}
}
}
}
S = T;
}
int ret = 0;
for (int j = 0 ; j < m; j ++) {
int cnt = 0;
int a = j;
for ( ; a ; cnt += (a&1) , a >>= 1);
if (cnt < k) continue;
for (int i = 0 ; i < sz ; i ++) {
ret += dp[S][i][j];
if (ret >= mod) {
ret -= mod;
}
}
}
return ret;
}
}AC;
int main() {
AC.Initialize();
int n , m , k;
while (~scanf("%d%d%d",&n,&m,&k)) {
if (n == 0 && m == 0 && k == 0) break;
AC.Reset();
for (int i = 0 ; i < m ; i ++) {
char temp[11];
scanf("%s",temp);
AC.Insert(temp , 1 << i);
}
AC.Construct();
printf("%d\n",AC.Work(n , 1 << m , k));
}
return 0;
}