P1314 聪明的质检员
题目描述:
小T是一名质量监督员,最近负责检验一批矿产的质量。这批矿产共有
𝑛 个矿石,从 1 到 𝑛逐一编号,每个矿石都有自己的重量 𝑤𝑖 以及价值
𝑣𝑖
检验矿产的流程是:
1.给定 𝑚 个区间 [𝑙𝑖,𝑟𝑖] ;
2.选出一个参数W;
3.对于一个区间 [𝑙𝑖,𝑟𝑖]计算矿石在这个区间上的检验值 𝑦𝑖。
yi 为 区间 [li, ri] 上的质量小于W矿石数量的总和与该区间上质量大于W的矿石的价值的总和的乘积。检验结果y即为每个区间的yi的和;若这批矿产的检验结果与所给标准值 𝑠相差太多,就需要再去检验另一批矿产。小T 不想费时间去检验另一批矿产,所以他想通过调整参数 𝑊 的值,让检验结果尽可能的靠近标准值 𝑠,即使得 ∣𝑠−𝑦∣ 最小。请你帮忙求出这个最小值。
输入格式:
第一行包含三个整数 𝑛,𝑚,𝑠,分别表示矿石的个数、区间的个数和标准值。接下来的 𝑛 行,每行两个整数,中间用空格隔开,第 𝑖+1 行表示 𝑖 号矿石的重量 𝑤𝑖 和价值 𝑣𝑖 。接下来的 𝑚 行,表示区间,每行两个整数,中间用空格隔开,第 𝑖+𝑛+1 行表示区间 [𝑙𝑖,𝑟𝑖] 的两个端点 𝑙𝑖 和 𝑟𝑖 。注意:不同区间可能重合或相互重叠。
输出格式:
一个整数,表示所求的最小值。
这个P1314数字还挺吉利的,就是硬是让我写了一晚上
题目解读 + 暴力算法:
这道题属实困扰了我好久,对于这道题我们先考虑暴力算法,暴力算法一般理解了题意就基本写得出来,我们知道这个W只能取在这些矿石的最大的质量以及最小的质量之间,我们需要找到这两个最大最小值,然后在这个区间内遍历一遍,每个W都有他固定的y值,而y值的计算只需按照题目所描述的计算方法按部就班就OK啦!
#include<bits/stdc++.h>
using namespace std;
#define N 200010
typedef long long ll;
ll n, m, s, l[N], r[N], w[N], v[N];// 这边定义一些基本的量,这里不过多赘述
ll maxw = 0, minw = 0x3f3f3f3f3f3f3f3f;//这边为了找出W的最大最小值确定区间
ll sum_wv(int W)//这个函数用来计算y的值
{
ll y = 0;//初始化y值为0;
for(int i = 1; i <= m; i ++)//遍历每个区间
{
ll sumw = 0, sumv = 0;//这个分别是题目所描述的区间内大于W的数量与其质量
for(int j = l[i]; j <= r[i]; j ++)//遍历该区间
{
if(w[j] >= W)//筛选掉那些不满足的矿石
{
sumw ++;
sumv += v[j];
}
}
y += sumw * sumv;//计算该区间的yi并加到y上
}
return y;//返回y值
}
int main()
{
scanf("%lld %lld %lld", &n, &m, &s);//这一部分是常规的输入不过多赘述了
for(int i = 1; i <= n; i ++)
{
scanf("%lld %lld", &w[i], &v[i]);
maxw = max(maxw, w[i]);//计算出W的左右边界
minw = min(minw, w[i]);
}
for(int j = 1; j <= m; j ++)
{
scanf("%lld %lld", &l[j], &r[j]);
}
ll ans = 0, min_ans = 0x3f3f3f3f3f3f3f3f;//这个是为了找出最优的W,以及|y - s|
for(int i = minw - 1; i <= maxw + 2; i ++)
{
int er = sum_wv(i);//这是把该W的y值赋值给er
if(abs(s - er) < min_ans)//打擂台不断地比较选出最优秀的那个
{
min_ans = abs(s - er);
ans = i;
}
}
cout << min_ans << endl;//输出
return 0;
}
TLE是很正常的,你看看这复杂度都多少了!!!!
优化方案:二分, 前缀和!
我们知道了一个W对应着一个y值,并且随着W的增加你筛选掉的矿石也只会越来越多,那么你的y值就会越来越小,那么现在你发现了终极规律,其实这个看似毫无规律的玩意儿,居然是有序的,那么接下来就该轮到今天的主角:二分,二分这东西很玄乎,就是像我这样的初学者很难看出来这是啥玩意,我们通过在minw与maxw之间二分选出最适合的y值,其最终原因是因为他是有序的,它能够根据W的值而改变,而我们只需找到最适合的W就行了,但是,如果只用二分很明显依旧还是很慢,无法通过所有的测试集。但当我拿出前缀和时阁下该如何应对,前缀和:是一种基础算法(不了解的可以去别处转转看,很好理解的不会花太多时间我这里就直接讲了)我们需要用前缀和求出区间内的数量和与价值和:我们直接先写个check函数:
long long check(int W) {
// 这里需要把sumw,与sumv两个前缀和数组全部初始化为零。
//memset当然也行
for (int i = 0; i <= n; ++i) {
sumw[i] = 0;
sumv[i] = 0;
}
for(int i = 1; i <= n; i ++) //这个就是计算前缀和的循环了
{
if(w[i] >= W) //只有当w[i]大于W才能允许加上
{
sumw[i] = sumw[i - 1] + 1;
sumv[i] = sumv[i - 1] + v[i];
}
else
{
sumw[i] = sumw[i - 1];
sumv[i] = sumv[i - 1];
}
}
// 计算检验结果 y
ans = 0;
for(int i = 1; i <= m; i++) //这个就是前缀和的运用了直接计算区间和
{
ans += (sumw[r[i]] - sumw[l[i] - 1]) * (sumv[r[i]] - sumv[l[i] - 1]);
}
return ans;//这个是y值
}
得到y值以后就直接进入二分内判断就OK啦,上代码!!!不废话了:
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
int n, m;
int w[N], v[N];
int l[N], r[N];
long long sumw[N], sumv[N];
long long ans, su, s;
long long check(int W) {
// 这里需要把sumw,与sumv两个前缀和数组全部初始化为零。
//memset当然也行
for (int i = 0; i <= n; ++i) {
sumw[i] = 0;
sumv[i] = 0;
}
for(int i = 1; i <= n; i ++) //这个就是计算前缀和的循环了
{
if(w[i] >= W) //只有当w[i]大于W才能允许加上
{
sumw[i] = sumw[i - 1] + 1;
sumv[i] = sumv[i - 1] + v[i];
}
else
{
sumw[i] = sumw[i - 1];
sumv[i] = sumv[i - 1];
}
}
// 计算检验结果 y
ans = 0;
for(int i = 1; i <= m; i++) //这个就是前缀和的运用了直接计算区间和
{
ans += (sumw[r[i]] - sumw[l[i] - 1]) * (sumv[r[i]] - sumv[l[i] - 1]);
}
return ans;//这个是y值
}
int main() {
cin >> n >> m >> s;
int maw = 0, miw = INT_MAX;
for(int i = 1; i <= n; i++) {
cin >> w[i] >> v[i];
maw = max(maw, w[i]);
miw = min(miw, w[i]);
}
for(int i = 1; i <= m; i++) {
cin >> l[i] >> r[i];
}
// 初始化 res 使得 res 是最大的方便后续打擂台
long long res = 0x3f3f3f3f3f3f3f3f;
// 二分搜索
int l = miw - 1, r = maw + 2;//这是矿物质量的范围
while(l <= r)
{
int mid = (l + r) >> 1;//基本模版
long long y = check(mid);
su = llabs(s - y);
res = min(res, su);
if(y > s) {//这段为了找出最接近s的y值不断地调整l与r的值
l = mid + 1;
} else {
r = mid - 1;
}
}
cout << res << endl;
return 0;
}
快乐ac吧!!!!!!!!!!!!!!!