大家好,歡迎來到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表示是否可以達成。
對於第一個樣例,我們一開始得到的陣列是[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;
}
線上變離線是競賽題當中非常常用的技巧,經常被用來解決一些查詢量非常大的問題。說穿了其實並不難,但是如果不知道想要憑自己幹想出來則有些麻煩。大家有時間,最好自己親自用程式碼實現體會一下。
今天的演算法題就聊到這裡,衷心祝願大家每天都有所收穫。如果還喜歡今天的內容的話,請來一個三連支援吧~(點贊、關注、轉發)