您的位置:首页 > 理论基础 > 数据结构算法

杂 - 数据结构 - 平衡树 - 权值线段树 - JZOJ 100036 随机

2017-08-28 21:31 387 查看
传送门

我居然在考场上想到了正解 = =。对于一个区间,当区间向右扩张时,差的绝对值(可能)会减小,而区间长度会扩大。发现,若差的绝对值已经小于等于区间长度,区间就没有必要继续向右扩张了,而应该考虑尽量把左边界向右靠。因此可以用两个指针来维护区间的左右端点,一直扫过去即可。这个操作的时间复杂度是O(n),想要过这道题,其附属操作的时间复杂度必须是O(logn)。

①一个自然的思路——平衡树(伪)

遇到过不少这样的题,可以用平衡树来维护第一个比自己大和第一个比自己小的数。我们用两棵平衡树来保存需要的数据。第一棵保存当前区间内存在的数,第二棵保存相邻元素的差值。其中,需要对元素进行去重,使第二棵树的最小值不可能是0;再加上一个是否出现过重复元素的标记,就能解决这个问题了。

这种做法还是有点复杂,这里就不多讲了(因为常数太大,过不了),放一个参考代码:

const INT maxn = INT(1e6) + 5;
INT n;
INT A[maxn];

typedef std::map<INT, INT>::iterator Iterator;
std::map<INT, INT> num, sub;

INT ans = ~(1ll << (sizeof(INT) * 8 - 1));

void run()
{
n = readIn();
for(int i = 1; i <= n; i++)
{
A[i] = readIn();
}

INT l = 1;
INT repeat = 0;
num[A[1]]++;
for(int i = 2; i <= n; i++)
{
num[A[i]]++;

Iterator it, pre, next, end;
pre = next = it = num.find(A[i]);
if(pre != num.begin())
pre--;
next++;
end = num.end();

INT oldSub;
INT newSub;
INT cntSub;
if(it->second == 1)
{
if(pre != it && next != end)
{
oldSub = next->first - pre->first;
sub[oldSub]--;
if(!sub[oldSub]) sub.erase(oldSub);
}

if(pre != it)
{
newSub = it->first - pre->first;
sub[newSub]++;
}
if(next != end)
{
newSub = next->first - it->first;
sub[newSub]++;
}
}
else if(it->second == 2) repeat++;

if(repeat) cntSub = 0;
else cntSub = sub.begin()->first;
ans = std::min(ans, std::max(cntSub, i - l + 1));
while(i - l > 1 && cntSub < i - l + 1)
{
pre = next = it = num.find(A[l]);
if(pre != num.begin())
pre--;
next++;
end = num.end();
l++;

if(it->second > 2) it->second--;
else if(it->second == 2)
{
repeat--;
it->second--;

if(!repeat)
{
cntSub = sub.begin()->first;
}
}
else
{
if(pre != it && next != end)
{
newSub = next->first - pre->first;
sub[newSub]++;
}

if(pre != it)
{
oldSub = it->first - pre->first;
sub[oldSub]--;
if(!sub[oldSub]) sub.erase(oldSub);
}
if(next != end)

4000
{
oldSub = next->first - it->first;
sub[oldSub]--;
if(!sub[oldSub]) sub.erase(oldSub);
}
num.erase(it);
}

if(repeat) cntSub = 0;
else cntSub = sub.begin()->first;
ans = std::min(ans, std::max(cntSub, i - l + 1));
}
}
cout << ans << endl;
}


肯定不想看是吧。。。其实不用看,这种东西只适合考场上拿来骗分,不适合二次阅读。

理论上这种算法是可以通过的(本机通过,其它地方打死过不了,看来学校这个机房的电脑强得不行啊!),但是常数可以说是大得惊人的,即使开了O2也没有什么大用处。(其实我在怀疑是不是输入太慢。。。)

②权值线段树

能不能考虑开一棵线段树呢?当然可以(废话 = =)。我们用一棵权值线段树,保存以下信息:

1>该区间出现数的次数

2>该区间差的绝对值的最小值(默认为防溢出型INF)

3>该区间最小的数和最大的数(默认为0)

然后你就惊奇的发现:我能算出来了!。。。为什么可以呢?

当写权值线段树时,请牢记:我是在写权值线段树,线段树上结点的位置是权值!所以当我们合并区间时,当前区间的 差的绝对值的最小值(以下简称答案) 无非有三种情况(分治思想,返璞归真):

1>继承左儿子的答案

2>继承右儿子的答案

3>右儿子中出现过的最小值减去左儿子中出现过的最大值

更新当前区间的最小值和最大值也很简单:

1>如果左儿子有最小值,当前区间的最小值就是左儿子的最小值(请记住这是权值线段树);如果没有,那就是右儿子的最小值;如果还是没有,就不管了。

2>最大值同理,我就不说了。。。

边界情况:

修改时:当区间变成点时,进行对应修改。如果当前位置没有数或者只有一个数,那么答案为INF,否则答案为0。如果当前位置没有数,那么最小值和最大值都为0(NULL),否则为当前位置(记住这是权值线段树)。

查询时:直接查询值域区间的答案即可,即tree[1].ans。

当然,搞事之前离散化一下是最好的,毕竟离散化很好写。鉴于并没有在这里上载过离散化代码,所以还是写一份:

const INT maxn = INT(1e6) + 5;
INT n, N;
INT A[maxn];
INT disc[maxn];
void discretize() //之后获取原值的方法:disc[A[i]]
{
for(int i = 1; i <= n; i++)
{
disc[i] = A[i];
}
std::sort(disc + 1, disc + 1 + n);
N = std::unique(disc + 1, disc + 1 + n) - (disc + 1); //下标从1开始
for(int i = 1; i <= n; i++)
{
A[i] = std::lower_bound(disc + 1, disc + 1 + N, A[i]) - disc; //注意与上面不一样,因为上面是为了获得个数,而这里是为了从1开始
}
}


现在解决了技术问题,我们再来看看这道题的思路对应的代码,应该就能一目了然了:

discretize();

INT l = 1;
st.insert(A[1]);
for(int i = 2; i <= n; i++)
{
st.insert(A[i]);
INT cntSub = st.query();
ans = std::min(ans, std::max(cntSub, i - l + 1));
while(i - l > 1 && cntSub < i - l + 1)
{
st.erase(A[l]);
l++;

cntSub = st.query();
ans = std::min(ans, std::max(cntSub, i - l + 1));
}
}
cout << ans << endl;


完整线段树代码(手渣,常数巨大,不开O2过不了,还好题目开O2):

class SegTree
{
#define PARAM INT node = 1, INT l = 1, INT r = N
#define DEF INT mid = (l + r) >> 1; INT lc = node << 1; INT rc = lc | 1;
struct Node
{
INT minPos;
INT maxPos;
INT minSub;
INT contain;

Node() : minSub(INF >> 1) {} //为了防止溢出除以一个2
} tree[maxn * 4];

INT g_Pos, g_Val;
void update(PARAM) //请与上面的叙述对应
{
DEF;
tree[node].minSub = std::min(tree[lc].minSub, tree[rc].minSub);
if(tree[lc].maxPos && tree[rc].minPos)
tree[node].minSub = std::min(tree[node].minSub, disc[tree[rc].minPos] - disc[tree[lc].maxPos]);

tree[node].minPos = tree[lc].minPos;
if(!tree[node].minPos) tree[node].minPos = tree[rc].minPos;

tree[node].maxPos = tree[rc].maxPos;
if(!tree[node].maxPos) tree[node].maxPos = tree[lc].maxPos;
}

void add_(PARAM)
{
if(l == r)
{
tree[node].contain += g_Val;
if(tree[node].contain) tree[node].minPos = tree[node].maxPos = l;
else tree[node].minPos = tree[node].maxPos = 0;

if(tree[node].contain >= 2) tree[node].minSub = 0;
else tree[node].minSub = INF;
return;
}
DEF;
if(g_Pos <= mid) add_(lc, l, mid);
if(g_Pos > mid) add_(rc, mid + 1, r);
update(node, l, r);
}

public:
INT query()
{
return tree[1].minSub;
}

void insert(INT x)
{
g_Pos = x;
g_Val = 1;
add_();
}
void erase(INT x)
{
g_Pos = x;
g_Val = -1;
add_();
}
} st;


这道题再一次展示了线段树的实用性和重要性。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: