缘木求鱼
题目链接:jzoj 7207
题目大意
定义 f(x) 函数为开一个数组大小为 x 的线段树,它的最大下标。
要你求 l~r 范围内 f(x)/x 的最大值。
思路
首先几个东西要知道。
- 线段树一个点左子树大小最多比右子树大小大一。
- 深度大的点比深度小的点编号大,如果相同深度,右边的点大。
- 大小为 x 的线段树的深度是 ⌈ log 2 x ⌉ \left\lceil\log_2^x\right\rceil ⌈log2x⌉
那首先我们想如何求
f
(
x
)
f(x)
f(x)。
首先确定最终方向,由于线段树是有关二进制的,而且数据范围是二进制给入,还是几十万位,所以我们是要从它的二进制上来找一些结论。
首先是最暴力的,从
1
1
1 开始一步一步走,那我们考虑能不能直接确定最优的在那一边。
(假设当前区间长度为
x
x
x,左儿子区间长度为
x
0
=
⌊
x
+
1
2
⌋
x0=\left\lfloor\dfrac{x+1}{2}\right\rfloor
x0=⌊2x+1⌋,右儿子区间长度为
x
1
=
⌊
x
2
⌋
x1=\left\lfloor\dfrac{x}{2}\right\rfloor
x1=⌊2x⌋)
然后结合上面的第二条,我们可以得出当且仅当
⌈
log
2
x
0
⌉
>
⌈
log
2
x
1
⌉
\left\lceil\log_2^{x0}\right\rceil>\left\lceil\log_2^{x1}\right\rceil
⌈log2x0⌉>⌈log2x1⌉ 时,最优值才会在左边。而且根据第一条,这个时候一定会有
x
=
2
k
+
1
(
k
⩾
1
)
x=2^k+1(k\geqslant1)
x=2k+1(k⩾1)。
然后你会发现在出现最高的两个
1
1
1 之前都是往右走,不会满足
2
k
+
1
2^k+1
2k+1 条件。
然后到了之后,因为一直在满足(除了最后一步),所以就是一直左走,最后右走。
那我们发现
f
(
x
)
f(x)
f(x) 只跟最高位的两个
1
1
1 有关系,如果设分别是
2
p
,
2
q
(
p
>
q
)
2^p,2^q(p>q)
2p,2q(p>q),我们还可以得出:
f
(
x
)
=
(
2
q
+
1
−
1
)
∗
2
p
−
q
∗
2
+
1
=
2
p
+
2
−
2
p
−
q
+
1
+
1
f(x)=(2^{q+1}-1)*2^{p-q}*2+1=2^{p+2}-2^{p-q+1}+1
f(x)=(2q+1−1)∗2p−q∗2+1=2p+2−2p−q+1+1
但是
x
x
x 不一定有两个
1
1
1 啊,如果只有一个
1
1
1 呢?
显然,那就是一直右走,设为
2
p
2^p
2p,那就是:
f
(
x
)
=
2
p
+
1
−
1
f(x)=2^{p+1}-1
f(x)=2p+1−1
然后我们就可以把第三档的部分分拿了。
接下来,我们才可以开始这题的第二档暴力。(笑死)
我们枚举 p , q p,q p,q( q q q 可能无),然后剩下的低位在 [ l , r ] [l,r] [l,r] 的前提下尽可能的小。
但是这时候又有问题了,我们要比较的是 f ( x ) x \frac{f(x)}{x} xf(x),做高精度小数除法是在想什么。
所以我们就考虑比较当前最优答案和当前要更新的答案。
f
(
x
)
x
<
f
(
y
)
y
\frac{f(x)}{x}<\frac{f(y)}{y}
xf(x)<yf(y)
f
(
x
)
y
<
f
(
y
)
x
f(x)y<f(y)x
f(x)y<f(y)x
然后由于 f ( x ) f(x) f(x) 只有两项或者三项,我们可以直接将其看做把 x / y 的二进制的两个 / 三个平移操作得到的二进制加起来,那就只有高精加和高精减了。
那计算 O ( n ) O(n) O(n),枚举 p , q p,q p,q 是 O ( n 2 ) O(n^2) O(n2),总的就是 O ( n 3 ) O(n^3) O(n3),我们就能过掉前三档啦。
然后就是最后一档啦,那我们就来慢慢的优化吧。
然后由于 1 ∼ 2 i 1\sim 2^i 1∼2i 的答案会比 1 ∼ 2 i − 1 1\sim 2^{i-1} 1∼2i−1 的优,我们可以限制 max { n , m − 1 } ⩽ p ⩽ m \max\{n,m-1\}\leqslant p\leqslant m max{n,m−1}⩽p⩽m,然后就变成 O ( n 2 ) O(n^2) O(n2) 的。
然后小小的证明:
你多了一层,你的点最大位置肯定会至少
∗
2
*2
∗2,那你数是最大乘了
∗
2
*2
∗2,所以结果只可能变大,不可能变小。
但是还是木大啊。
我们考虑减少决策点( q q q) 的选择。
首先我们保证 l , r l,r l,r 二进制位数相同,如果不同,我们可以把它分成两部分(前面说了位数可以变成至多只差一位),然后两个的答案取最优的。
然后我们把
l
,
r
l,r
l,r 用二进制表示出来。
l
=
10...01...
l=10...01...
l=10...01...
r
=
10...10...
r=10...10...
r=10...10...
那如果这里搞 LCP,LCP 里面有超过一个
1
1
1,那第二个
1
1
1 的范围就直接限死了,就直接是
l
l
l。
那否则我们看第二个
1
1
1 放在哪里。
那假设两个的第二个一分别是
u
,
v
u,v
u,v。(
r
r
r 找到的是
v
v
v,
l
l
l 找到的是
u
u
u,
u
⩽
v
u\leqslant v
u⩽v)
那我们的
q
q
q 就只能在
[
u
,
v
]
[u,v]
[u,v] 中,如果是
u
u
u,那就是
l
l
l,放在其它位置(
(
u
,
v
]
(u,v]
(u,v])的时候,后面的位都放
0
0
0。
那除了
u
u
u,别的都可以以
x
=
2
p
+
2
q
x=2^p+2^q
x=2p+2q 的形式表示出来。
可以通过打表找规律和进行导数练习得到
p
p
p 固定的时候,
q
q
q 接近
p
/
2
p/2
p/2 最优。
(你也可以自己用电脑画个图像看看)
然后你的决策点就变得很少。
(注意一些左端点在
p
/
2
p/2
p/2 右边,就只能选左端点;右端点在
p
/
2
p/2
p/2 左边,就只能选右端点)
(然后为了以防万一,我们会把
l
l
l 的答案也算上)
然后各种情况处理一下计算一下就好了。
然后决策点就只剩下两三个左右,就变成 O ( n ) O(n) O(n) 可以过啦。
代码
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int n, m, ansn;
char L[2000001], R[2000001], mid[2000001];
int ans[2000001], ans_[2000001], tmp[2000001];
int x_[4000001], y_[4000001], opx[4], opy[4];
int degx[4], degy[4], xn, yn;
void jia(int *x, int *y, int deg) {
int len = deg + y[0], w = 0;
for (int i = deg + 1; i <= len; i++) {
x[i] += w + y[i - deg];
w = x[i] / 2;
x[i] %= 2;
}
while (w) {
x[++len] = w;
w = x[len] / 2;
x[len] /= 2;
}
if (x[0] < len) x[0] = len;
}
void jian(int *x, int *y, int deg) {
int len = deg + y[0], w = 0;
for (int i = deg + 1; i <= len; i++) {
x[i] -= w + y[i - deg];
w = (x[i] < 0);
x[i] &= 1;
}
while (w) {
x[++len] -= w;
w = (x[len] < 0);
x[len] &= 1;
}
while (x[0] > 0 && !x[x[0]]) x[0]--;
}
void get_good(int *x, int *y) {
if (x[0] == y[0]) {//相同
bool same = 1;
for (int i = 1; i <= x[0]; i++)
if (x[i] != y[i]) {
same = 0; break;
}
if (same) return ;
}
for (int i = 1; i <= x_[0]; i++) x_[i] = 0;
for (int i = 1; i <= y_[0]; i++) y_[i] = 0;
x_[0] = y_[0] = 0;
xn = yn = 0;
int firx = x[0] + 1, firy = y[0] + 1;
for (int i = 2; i <= x[0]; i++)
if (x[x[0] - i + 1]) {
firx = i; break;
}
for (int i = 2; i <= y[0]; i++)
if (y[y[0] - i + 1]) {
firy = i; break;
}
if (firx == x[0] + 1) {//2^0+2^1+...+2^p
degx[++xn] = x[0]; opx[xn] = 1;
degx[++xn] = 0; opx[xn] = -1;
}
else {//2^(p+2)-2^(p-q+1)+1
degx[++xn] = x[0] + 1; opx[xn] = 1;
degx[++xn] = firx; opx[xn] = -1;
degx[++xn] = 0; opx[xn] = 1;
}
if (firy == y[0] + 1) {
degy[++yn] = y[0]; opy[yn] = 1;
degy[++yn] = 0; opy[yn] = -1;
}
else {
degy[++yn] = y[0] + 1; opy[yn] = 1;
degy[++yn] = firy; opy[yn] = -1;
degy[++yn] = 0; opy[yn] = 1;
}
for (int i = 1; i <= xn; i++) {//移位加减
if (opx[i] == 1) jia(x_, y, degx[i]);
else jian(x_, y, degx[i]);
}
for (int i = 1; i <= yn; i++) {
if (opy[i] == 1) jia(y_, x, degy[i]);
else jian(y_, x, degy[i]);
}
if (x_[0] < y_[0]) {//比较,如果 y_ 优就交换
int maxn = max(x[0], y[0]);
for (int i = 0; i <= maxn; i++)
swap(x[i], y[i]);
}
else if (x_[0] == y_[0]) {
for (int i = x_[0]; i >= 1; i--) {
if (x_[i] > y_[i]) return ;
if (x_[i] < y_[i]) {
int maxn = max(x[0], y[0]);
for (int i = 0; i <= maxn; i++)
swap(x[i], y[i]);
return ;
}
}
}
}
void get_ans(char *l, char *r, int n) {
int now = 1, onenum = 0;
while (now <= n && l[now] == r[now]) onenum += (l[now++] == '1');
if (onenum > 1) {//lcp 超过 1 个 1
for (int i = 1; i <= n; i++)
ans[n - i + 1] = l[i] - '0';
ans[0] = n;
return ;
}
int lfir = n + 1, rfir = n + 1;
for (int i = 2; i <= n; i++)//各自找到第二个 1
if (l[i] == '1') {
lfir = i; break;
}
for (int i = 2; i <= n; i++)
if (r[i] == '1') {
rfir = i; break;
}
int p2 = n / 2 + 1;
bool ltwo = 0;
for (int i = 2; i <= n; i++)
if (l[i] == '1') {
ltwo = 1; break;
}
for (int i = 1; i <= n; i++)//先试一下选 l 的
ans[n - i + 1] = l[i] - '0';
ans[0] = n;
if (rfir <= p2 && p2 <= lfir) {//可以选到 p/2
if (p2 == lfir && ltwo) {//会被 >=l 限制(选l)(前面处理了所以就不用再弄)
if (rfir < lfir && lfir > 2) {//因为会限制所以考虑用两个 1 的方法选后面可以选的那个
for (int i = 1; i <= n; i++)
tmp[i] = 0;
tmp[n - (p2 - 1) + 1] = tmp[n - 1 + 1] = 1;
tmp[0] = n;
get_good(ans, tmp);
}
return ;
}
for (int i = 1; i <= n; i++)//普通的选 p/2
tmp[i] = 0;
tmp[n - p2 + 1] = tmp[n - 1 + 1] = 1;
tmp[0] = n;
get_good(ans, tmp);
return ;
}
else if (p2 > lfir) {//l 那边超了,限制 l
for (int i = 1; i <= n; i++)
tmp[i] = 0;
if (ltwo) tmp[n - (lfir - 1) + 1] = 1;
else tmp[n - lfir + 1] = 1;
tmp[n - 1 + 1] = 1;
tmp[0] = n;
get_good(ans, tmp);
return ;
}
else if (rfir > p2) {//r 那边超了
for (int i = 1; i <= n; i++)
tmp[i] = 0;
tmp[n - rfir + 1] = tmp[n - 1 + 1] = 1;
tmp[0] = n;
get_good(ans, tmp);
return ;
}
}
int main() {
scanf("%d %s %d %s", &n, L + 1, &m, R + 1);
if (n + 1 < m) {//直接处理最后两位的(1~2^(i+1) 答案比 1~2^i 的优)
n = m - 1;
L[1] = '1';
for (int i = 2; i <= n; i++) L[i] = '0';
}
if (n + 1 == m) {//要拆成两个相同为的
for (int i = 1; i <= n; i++)
mid[i] = '1';
get_ans(L, mid, n); swap(ans_, ans);
mid[1] = '1';
for (int i = 2; i <= m; i++)
mid[i] = '0';
get_ans(mid, R, m); get_good(ans, ans_);
}
else {//直接做
get_ans(L, R, n);
}
for (int i = 1; i <= ans[0]; i++)//这里倒叙储存,所以要倒叙输出(字符串是正序,数字的是倒叙)
printf("%d", ans[ans[0] - i + 1]);
return 0;
}