题目链接:https://www.luogu.com.cn/problem/P1314
题目大意:现有n个矿石从左到右排列,每个矿石有自己的重量
w
i
w_i
wi和价值
v
i
v_i
vi,现给定
m
m
m个区间
[
l
i
,
r
i
]
[l_i,r_i]
[li,ri]和预期标准值
S
S
S,我们需要选出一个参数
W
W
W,使每个区间检验值
y
i
y_i
yi之和最接近预期标准值
S
S
S(即作差的绝对值
∣
s
−
y
∣
|s-y|
∣s−y∣最小),检验值的计算方法如下:
对于一个区间而言:
y
i
=
∑
j
=
l
i
r
i
[
w
j
≥
W
]
×
∑
j
=
l
i
r
i
[
w
j
≥
W
]
v
j
y_i=\sum\limits^{r_i}_{j=l_i}[w_j \geq W]\times\sum\limits^{r_i}_{j=l_i}[w_j \geq W]v_j
yi=j=li∑ri[wj≥W]×j=li∑ri[wj≥W]vj
这个算式可以这样理解:一个区间中重量大于标准值的个数乘总价值。
(到这里前缀和求区间和的思路就比较明显了,先往下看)
数据范围如下图所示:
对于
100
%
100\%
100%的数据,需要我们用
O
(
n
log
n
)
O(n\log n)
O(nlogn)的算法,我们自然可以想到二分法。对于二分答案法,我们首先需要考虑单调性,我们可以知道,给出的矿石重量是一定的,当我们增加参数
W
W
W的大小时,每个单调区间的满足
w
j
≥
W
w_j\geq W
wj≥W的矿石数量与增加前一定减少或相等,那么每个区间的检验值一定减少或相等。我们可以有以下结论:
选取的参数越高,我们得到的检验结果 y y y(即所有区间检验值之和)越小。这个具有单调性的结论说明了我们使用二分答案做法的合理性。
那么我们二分答案的边界修改应该如何操作呢?在一开始时,我们定义
l
=
1
l=1
l=1,
r
=
+
i
n
f
r=+inf
r=+inf,
m
i
d
=
l
+
r
>
>
1
mid=l+r>>1
mid=l+r>>1。在题目中我们需要尽量地靠近一个值
s
s
s,也就是尽可能地减小绝对值。
当基于
W
=
m
i
d
W=mid
W=mid的检测结果
y
y
y的值比预期标准值
s
s
s大时,说明更优的答案应当在
m
i
d
+
1
mid+1
mid+1和
r
r
r之间,修改边界:
l
=
m
i
d
+
1
l=mid+1
l=mid+1,并比较
W
W
W与当前最优答案
a
n
s
ans
ans的值。
当基于
W
=
m
i
d
W=mid
W=mid的检测结果
y
y
y的值比预期标准值
s
s
s小时,说明更优的答案应当在
l
l
l和
m
i
d
−
1
mid-1
mid−1之间,修改边界:
r
=
m
i
d
−
1
r=mid-1
r=mid−1,并比较
W
W
W与当前最优答案
a
n
s
ans
ans的值。
当
y
=
=
s
y==s
y==s时,直接输出0然后结束程序。
这样我们的二分答案的思路就有了。考虑在
O
(
N
)
O(N)
O(N)的时间复杂度内计算出给定
W
W
W时的检测结果
y
y
y。我们进一步简化问题,在
W
W
W已知的情况下,我们对与
W
W
W有关的特殊数组进行m次区间查询。问题的重心就转移到如何在O(1)的时间复杂度计算出一个区间。此时我们就应该联想到一些特殊的数据处理方法,诸如前缀和,差分,树状数组,线段树等等,这里肯定不会大材小用后两个,后两个的区间查询复杂度也比较高。这里直接引入前缀和,为什么是前缀和呢?
假设当前有这样的一个原始数组。
我们做这样的处理后获得前缀和数组,该数组每个元素
s
u
m
[
i
]
=
a
[
1
]
+
a
[
2
]
+
.
.
.
+
a
[
i
]
sum[i]=a[1]+a[2]+...+a[i]
sum[i]=a[1]+a[2]+...+a[i]。这样处理的好处是什么呢?我们有了这个数组,就可以在O(1)的复杂度内求出原始数组的一个区间和。即
a
[
i
]
+
a
[
i
+
1
]
+
a
[
i
+
2
]
+
.
.
.
+
a
[
j
−
1
]
+
a
[
j
]
=
s
u
m
[
j
]
−
s
u
m
[
i
−
1
]
,
(
i
<
j
)
a[i]+a[i+1]+a[i+2]+...+a[j-1]+a[j]=sum[j]-sum[i-1], (i<j)
a[i]+a[i+1]+a[i+2]+...+a[j−1]+a[j]=sum[j]−sum[i−1],(i<j),便求出了原始数组中
[
i
,
j
]
[i,\space j]
[i, j]的区间数值之和
我们观察检验值的计算公式,其中包含有两个未知项,分别是一个满足条件的数量区间和与一个满足条件的价值区间和,我们分两个数组 i t e m s u m [ i ] item_sum[i] itemsum[i]和 v a l u e s u m [ i ] value_sum[i] valuesum[i]分别储存两种前缀和。具体的处理方法是这样:
for(int i=1;i<=n;i++)
{
if(w[i]>=x)
{
value_sum[i]=value_sum[i-1]+1;
item_sum[i]=item_sum[i-1]+v[i];
}
else
{
value_sum[i]=value_sum[i-1];
item_sum[i]=item_sum[i-1];
}
}
可以看出,我们遍历了两个前缀和数组,若遍历到当前的矿石满足重量条件,则将他的值加入前缀和数组中,若不满足,则保持之前的前缀和数组不变。这样我们就完成了前缀和数组的初始化。
接下来就是查询操作,这一步很简单:
long long cal()
{
long long ans=0;
for(int i=1;i<=m;i++)
{
ans+=(item_sum[r[i]]-item_sum[l[i]-1])*(value_sum[r[i]]-value_sum[l[i]-1]);
}
return ans;
}
如上,每个区间的查询结果就是两个区间的前缀和相乘,把所有结果相加就是取当前参数 W W W的检验值。记得开 l o n g l o n g long\space long long long,不然会炸裂。计算结束后回到之前的二分思路,就完成解题了。
下面给出AC代码:
#include<bits/stdc++.h>
using namespace std;
int w[200200],v[200200];
int l[200200],r[200200];
int n,m;
long long s;
long long item_sum[200200],value_sum[200200];
int max_w=-1;
long long min(long long x,long long y)
{
if(x>y) return y;
else return x;
}
void modify(int x)
{
for(int i=1;i<=n;i++)
{
if(w[i]>=x)
{
value_sum[i]=value_sum[i-1]+1;
item_sum[i]=item_sum[i-1]+v[i];
}
else
{
value_sum[i]=value_sum[i-1];
item_sum[i]=item_sum[i-1];
}
}
}
long long cal()
{
long long ans=0;
for(int i=1;i<=m;i++)
{
ans+=(item_sum[r[i]]-item_sum[l[i]-1])*(value_sum[r[i]]-value_sum[l[i]-1]);
}
return ans;
}
int main()
{
cin>>n>>m>>s;
for(int i=1;i<=n;i++)
{
cin>>w[i]>>v[i];
max_w=max(max_w,w[i]);
}
for(int i=1;i<=m;i++)
{
cin>>l[i]>>r[i];
}
int begin=1,end=max_w+1,mid;
long long anss=9999999999999999;
while(begin<=end)
{
mid=begin+end>>1;
modify(mid);
long long ans=cal();
if(ans>=s)
{
begin=mid+1;
}
else
{
end=mid-1;
}
anss=min(anss,fabs(s-ans));
}
cout<<anss;
}
有疏漏和讲解不清的地方请私信留言,祝你新的一天RP++。