codeforces 1461D,离线查询是什么神仙方法,为什么快这么多?
大家好,欢迎来到codeforces专题。
今天我们选择的题目是1461场次的D题,这题全场通过了3702人,从难度上来说比较适中。既没有很难,也很适合同学们练手。另外它用到了一种全新的思想是在我们之前的文章当中没有出现过的,相信对大家会有一些启发。
链接:https://codeforces.com/contest/1461/problem/D
废话不多说了,让我们开始吧。
题意
我们给定包含n个正整数的数组,我们可以对这个数组执行一些操作之后,可以让数组内元素的和成为我们想要的数。
我们对数组的执行操作一共分为三个步骤,第一个步骤是我们首先计算出数组的中间值mid。这里mid的定义不是中位数也不是均值,而是最大值和最小值的均值。也就是mid = (min + max) / 2。
得出了mid之后,我们根据数组当中元素的大小将数组分成两个部分。将小于等于mid的元素分为第一个部分,将大于mid的元素分为第二个部分。这样相当于我们把原来的大数组转化成了两个不同的小数组。
现在我们一共有q个请求,每个请求包含一个整数k。我们希望程序给出我们能否通过上述的操作使得最终得到的数组内的元素和等于k。
如果可以输出Yes,否则输出No。
样例
首先输入一个整数t,表示测试数据的组数()。
对于每一组数据输入两个整数n和q,n表示数组内元素的数量,q表示请求的数量()。接着第二行输入一行n个整数,其中的每一个数,都有。
接下来的q行每行有一个整数,表示我们查询的数字k(),保证所有的n和q的总和不超过。
对于每一个请求我们输出Yes或No表示是否可以达成。
![](https://tva1.sinaimg.cn/large/0081Kckwgy1glm8y7lqa6j31d20r4myr.jpg)
对于第一个样例,我们一开始得到的数组是[1, 2, 3, 4, 5]
。我们第一次执行操作,可以得到mid = (1 + 5) / 2 = 3。于是数组被分为[1, 2, 3]
和[4, 5]
。对于[1, 2, 3]
继续操作,我们可以得到mid = (1 + 3) / 2 = 2,所以数组可以分成[1, 2]
和[3]
。[1, 2]
最终又可以拆分成[1]
和[2]
。
我们可以发现能够查找到的k为:[1, 2, 3, 4, 5, 6, 9, 15]
。
题解
这道题并不算很复杂,解法还是比较清晰的。
我们很容易发现对于数组的操作其实是固定的,因为数组当中的最大值和最小值都是确定的。我们只需要对数组进行排序之后,通过二分查找就可以很容易完成数组的拆分。同样,对于数组的求和我们也不用使用循环进行累加运算,通过前缀和很容易搞定。
所以本题唯一的难度就只剩下了如何判断我们要的k能不能找到,其实这也不复杂,我们只需要把它当成搜索问题,去搜索一下所有可以达到的k即可。这个是基本的深搜,也没有太大的难度。
bool examine(int l, int r, int k) {
if (l == r) return (tot[r] - tot[l-1] == k);
// 如果[l, r]的区间和已经小于k了,那么就没必要去考虑继续拆分了
if (l > r || tot[r] - tot[l-1] < k) {
return false;
}
if (tot[r] - tot[l-1] == k) {
return true;
}
// 中间值就是首尾的均值
int m = (nums[l] + nums[r]) / 2;
// 二分查找到下标
int md = binary_search(l, r+1, m);
if (md == r) return false;
return examine(l, md, k) | examine(md+1, r, k);
}
这段逻辑本身并不难写,但是当我们写出来之后,发现仍然不能AC,会超时。我当时思考了很久,终于才想明白问题出在哪里。
问题并不是我们这里搜索的复杂度太高,而是搜索的次数太多了。q最多情况下会有,而每次搜索的复杂度是。因为我们的搜索层数是,加上我们每次使用二分带来的,所以极端的复杂度是,在n是的时候,这个值大概是,再加上一些杂七杂八的开销,所以被卡了。
为了解决这个问题,我们引入了离线机制。
这里的离线在线很好理解,所谓的在线查询,也就是我们每次获得一个请求,查询一次,然后返回结果。而离线呢则相反,我们先把所有的请求查询完,然后再一个一个地返回。很多同学可能会觉得很诧异,这两者不是一样的么?只不过顺序不同而已。
大多数情况下的确是一样的,但有的时候,我们离线查询是可以批量进行的。比如这道题,我们可以一次性把所有可以构成的k通过一次递归全部查出来,然后存放在set中。之后我们只需要根据输入的请求去set当中查询是否存在就可以了,由于查询set的速度要比我们通过递归来搜索快得多。这样就相当于将q次查询压缩成了一次,从而节约了运算的时间,某种程度上来说也是一种空间换时间的算法。
我们来看代码,获取更多细节:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
#include <vector>
#include <cmath>
#include <cstdlib>
#include <string>
#include <map>
#include <set>
#include <algorithm>
#include "time.h"
#include <functional>
#define rep(i,a,b) for (int i=a;i<b;i++)
#define Rep(i,a,b) for (int i=a;i>b;i--)
#define foreach(e,x) for (__typeof(x.begin()) e=x.begin();e!=x.end();e++)
#define mid ((l+r)>>1)
#define lson (k<<1)
#define rson (k<<1|1)
#define MEM(a,x) memset(a,x,sizeof a)
#define L ch[r][0]
#define R ch[r][1]
const int N=100050;
const long long Mod=1000000007;
using namespace std;
int nums[N];
long long tot[N];
set<long long> ans;
int binary_search(int l, int r, int val) {
while (r - l > 1) {
if (nums[mid] <= val) {
l = mid;
}else {
r = mid;
}
}
return l;
}
// 离线查询,一次把所有能构成的k放入set当中
void prepare_ans(int l, int r) {
if (l > r) return ;
if (l == r) {
ans.insert(nums[l]);
return ;
}
ans.insert(tot[r] - tot[l-1]);
int m = (nums[l] + nums[r]) / 2;
int md = binary_search(l, r+1, m);
if (md == r) return ;
prepare_ans(l, md);
prepare_ans(md+1, r);
}
int main() {
int t;
scanf("%d", &t);
rep(z, 0, t) {
ans.clear();
MEM(tot, 0);
int n, q;
scanf("%d %d", &n, &q);
rep(i, 1, n+1) {
scanf("%d", &nums[i]);
}
sort(nums+1, nums+n+1);
rep(i, 1, n+1) {
tot[i] = tot[i-1] + nums[i];
}
prepare_ans(1, n);
rep(i, 0, q) {
int k;
scanf("%d", &k);
// 真正请求起来的时候,我们只需要在set里找即可
if (ans.find(k) != ans.end()) {
puts("Yes");
}else {
puts("No");
}
}
}
return 0;
}
在线变离线是竞赛题当中非常常用的技巧,经常被用来解决一些查询量非常大的问题。说穿了其实并不难,但是如果不知道想要凭自己干想出来则有些麻烦。大家有时间,最好自己亲自用代码实现体会一下。
今天的算法题就聊到这里,衷心祝愿大家每天都有所收获。如果还喜欢今天的内容的话,请来一个三连支持吧~(点赞、关注、转发)