我們二分答案, 有多少套牌.
然後如果二分出來的是 x套牌, 我們檢查, 組成x套牌需要用到多少張joker, 如果joker的數量超過x, 說明同一個組合裡用到了超過1張的joker. 我們還要檢查使用的joker 有沒有超過題目限定的m.
給定每天有多少個教室可以借. 和. 每天有多少份訂單(包括借出和反還時間 和 數量).
暴力思路, 直接遍歷每天的訂單 同時 修改每天可以借出的教室, 用 O(n * m * (t - s)),這個顯然會超時.
由於我們知道, 如果某一個天所有教室都借出去了, 然後有人再借那麼他就會是負數, 然後他後面所有的天數都是不滿足條件的.
我們考慮二分最後一個可行的天數.
利用差分數組對 區間內的數字進行修改, 看看有沒有負數, 如果有負數, 表示 答案在前面, 如果沒有負數 表示答案在後面.
為了防止出錯, 我們把借出時間 做一個排序.
- #include<bits/stdc++.h>
- using namespace std;
- int n, m;
- struct node{
- int l, r, d;
- }a[10000000];
- long long r[1000000];
- long long del[1000000];
- bool check(int x){
- for(int i = 1; i <= n; i++){
- del[i] = r[i] - r[i - 1];
- }
- for(int i = 1; i <= x; i++){
- del[a[i].l] -= a[i].d;
- del[a[i].r + 1] += a[i].d;
- }
- long long sum = 0;
- for(int i = 1; i <= n; i++){
- sum += del[i];
- if(sum < 0)return false;
- }
- return true;
- }
- int main(){
- cin >> n >> m;
- for(int i = 1; i <= n; i++){
- cin >> r[i];
- }
- for(int i = 1; i <= m; i++){
- cin >> a[i].d >> a[i].l >> a[i].r;
- }
- int l = 0, r = m;
- while(l <= r){
- int mid = (l + r) / 2;
- if(check(mid))l = mid + 1;
- else r = mid - 1;
- }
- if(l > m)cout << 0 << endl;
- else cout << -1 << endl << l << endl;
- return 0;
- }
這裡要注意一個點. 就是我們的差分數組 每次都重新計算, 因為我們現在是要計算到底 x 是不是一個合法日期, 而不是整個所有日子是不是都是合法日期.
最簡單的想法是直接枚舉第m大的可能.
因為我們知道第m大 x 意味著 他必須有m - 1 個數比x要大. 那麼我們直接去找所有區間看看 我們枚舉的x 是不是有m - 1個第k大比他大.
那麼我們知道其實這是一個單調性問題. 所以我們選擇二分這個x的可能性.
那麼我們怎麼去統計有多少個區間的第k大比x 要大呢?
我們對給定的數組進行排序, 然後一旦我們在某一區間[l , r]中發現剛剛好裡面有k個數字比x大, 那麼我們就知道 第k大的數不可能是x, 而且他會比x大. 就可以直接統計區間的數量 用n - r + 1.
這裡就是普通的尺取法了.
- #include <bits/stdc++.h>
- using namespace std;
- const int N=1e5+500;
- typedef long long ll;
- ll n,k,m;
- int w[N],b[N];
-
- bool check(int u)//双指针检测就好了.
- {
- int l=1,r=0;//维护它为第k大的区间.
- int num=0;//维护这个区间里有多少小于等于u的数.
- ll res=0;
- while(l<=n){
- //統計比u大的數有多少個
- while(r<=n&&num<k){
- if(w[++r]>=u)num++;
- }
- //一旦有k個 立馬結算
- if(num==k)res+=n-r+1;
- //尋找下一個區間
- if(w[l]>=u){
- num--;
- }
- l++;
- }
- return res>=m;
- }
-
- int main()
- {
- std::ios::sync_with_stdio(false);
- std::cin.tie(0);
- int T;
- cin >> T;
- while(T--)
- {
- cin >> n >> k >> m;
- for(int i=1;i<=n;i++){
- cin >> w[i];
- b[i]=w[i];
- }
- //去掉連續出現的相同數字.
- int r=unique(b+1,b+1+n)-b-1;
- int l=1;
- sort(b+1,b+1+r);
- int ans=0;
- while(l<=r)
- {
- int mid=(l+r)>>1;
- if(check(b[mid]))//当我二分的这个值数量大于等于m时,要放大.
- {
- l=mid+1;
- ans=max(ans,b[mid]);
- }
- else r=mid-1;//否则要缩小
- }
- printf("%d\n",ans);
- }return 0;
- }
如果有一個函數 存在峰值, 我們先求 l 到 mid 的mid 我們姑且叫他做lmid, 同理求出rmid, 然後如果lmid 比 rmid大證明, 峰值不可能存在於rmid 到r的區間, 我們捨去他. 如此類推 直到求出峰值.
如何求出lmid?
我先把l 到 r 等分分割三段. 让l + 上这个长度, 就算到lmid 了, rmid 就是让r - 上这个长度.
三分的模板

- #include<cstdio>
- #include<cmath>
- using namespace std;
- const double eps=1e-5;
- double a,b,c,xx,yy;
- double f(double x){
- return sqrt((x-xx)*(x-xx)+(a*x*x+b*x+c-yy)*(a*x*x+b*x+c-yy));
- }
- int main(){
- scanf("%lf%lf%lf%lf%lf",&a,&b,&c,&xx,&yy);
- double l=-200,r=200;
- while(r-l>eps){
- double lm=l+(r-l)/3,rm=r-(r-l)/3;
- if(f(lm)<f(rm)) r=rm;
- else l=lm;
- }
- printf("%.3lf\n",f(l));
- return 0;
- }
例题:
给定二维坐标图上若干个点, 求出最小能包含所有点的圆半径.
观察发现, 如果不考虑y坐标, 只考虑x坐标, 假设我们的答案是m, 如果m 往左移, 或右移, 答案都会增大, 不会减少, 因为m已经是最优质的. 我们发现他是一个单峰值函数. 可以考虑3分解法.
对于y坐标同理.
现在唯一需要考虑的点是, 最优值的y 所构造出的最优质的x, 是不是就是全局最优?
这是正确的. 因为这个关系是一个三维关系, x 不变 会产生一个f(y)的函数

随着不同的x 产生, 会有这样一个图.
反正就是随着x的增大, 这个图形就像是一个往里凹的腕一样. 当找到y的最优值, 再找x的最优质, 必然是最低点.
学过偏微分就明白了.
AB 之间存在一个最优点, 只要离这个点越远, 时间增加的越多.
同理 CD野存在.
所以我们可以三分AB 之间的一个最优点E, 和三分CD之间的最优点F. 就可以找到答案了.
f(E, F) = dis(A, E)/p + dis(F, D) / q + dis(E, F) / r
三分套三分的一道题目
二分答案, 进行公示移项, 试错.
- #include<bits/stdc++.h>
- using namespace std;
- #define int long long
- int n, k;
- struct node{
- int c, v, tmp;
- }a[10000000];
- bool cmp(node a, node b){
- return a.tmp > b.tmp;
- }
- bool check(int x){
- for(int i = 1; i <= n; i++){
- a[i].tmp = a[i].v - a[i].c * x;
- }
- sort(a + 1, a + 1 + n, cmp);
- int sum = 0;
- for(int i = 1; i <= k; i++){
- sum += a[i].tmp;
- }
- return sum >= 0;
- }
- signed main(){
- int T;
- cin >> T;
- while(T--){
- cin >> n >> k;
- for(int i = 1; i <= n; i++){
- cin >> a[i].c >> a[i].v;
- }
- int ans = 0;
- int l = 1, r = 1e8 + 1000;
- while(l <= r){
- int mid = (l + r) / 2;
- if(check(mid)){
- l = mid + 1;
- ans = mid;
- }else{
- r = mid - 1;
- }
- }
- cout << ans << endl;
- }
- return 0;
- }
二分答案
題目說時間從0開始累積, 會彈奏某一個音符b次, 也就是b秒.
然後題目問某一個時間點彈到哪個音符.
其實不難, 由於彈奏是一個累積的數組, 我們先求前綴和. 然後, 每一個前綴和代表彈第bi個音符的時候的時間是多少.
題目給了一個時間點t, 也就是說我們在這個前綴和中找出第一個大於或等於t的時間點中的索引就是答案的音符.
- #include<iostream>
- #include<algorithm>
- using namespace std;
- int main(){
- int n, q;
- cin >> n >> q;
- int b[n + 1];
- b[0] = 0;
- for(int i = 1; i <= n; i++){
- cin >> b[i];
- b[i] = b[i - 1] + b[i];
- }
- while(q--){
- int x;
- cin >> x;
- cout << (upper_bound(b + 1, b + 1 + n, x) - b) << endl;
- }
- return 0;
- }

由於10億開方只有30000多, 我們大可以創建一個數組, 把1 到30000全部放在裡面.
由於裡面全部都是實數, 所以每一個數字他的2次方都是完全平方數. 因此我們只需要找到 根號L到根號R之間有多少個數字就可以了. 最簡單的方法就是找第一個小於或等於根號L的數字, 和 第一個大於或等於根號R的地方, 相減就是完全平方數的數量了
- #include<bits/stdc++.h>
- using namespace std;
- const int N =31625;
- int a[N];
- int main()
- {
- for(int i=0;i<N;++i)a[i]=i;
- int n,l,r;
- cin>>n;
- while(n--){
- cin>>l>>r;
- int L=0,R=N;
- L=lower_bound(a,a+N,sqrt(l))-a;
- R=upper_bound(a,a+N,sqrt(r))-a;
- cout<<max(R-L,0)<<endl;
- }
- return 0;
- }
我們二分一個每天最大可以保持的快樂值.
拿到這個值我們就去check 到底可不可以保持.
由於每天都會快樂減半, 所以一旦我們發現快樂值低於二分出來的x, 我們就不斷吃巧克力直到快樂水平達到或超過x.
由於巧克力的數量有限, 一旦發現巧克力吃完了但還是無法達到x水平, 證明這個x 是不可能成為合法的快樂值的.
由於答案要我們同時輸出, 到底哪一天吃了哪些巧克力, 我們可以在吃巧克力讓快樂水平回到x的時候跟新一個答案 數組.
- #include <iostream>
-
- using namespace std;
-
- typedef long long LL;
- const int N = 1e6 + 10;
-
- int n, d;
- LL level;
- LL a[N], c[N];
-
- bool check(LL x)
- {
- LL sum = 0, t = 0;
- for(int i = 1; i <= d; i ++)
- {
- while(sum < x)
- {
- sum += a[++t];
- if(t > n) return false;
- if(x == level) c[t] = i;
- }
- sum /= 2;
- }
-
- return true;
- }
-
- int main()
- {
- cin >> n >> d;
- for(int i = 1; i <= n; i ++) cin >> a[i];
-
- LL l = 0, r = 1e11;
- LL ans = 0;
- while(l <= r)
- {
- LL mid = l + r + 1 >> 1;
- if(!check(mid)) r = mid - 1;
- else{ l = mid + 1;
- ans = mid;}
- }
-
- level = ans;
- check(level);
- cout << level << endl;
- for(int i = 1; i <= n; i ++)
- {
- if(c[i]) cout << c[i] << endl;
- else cout << d << endl;
- }
-
- return 0;
- }
直接二分這個最短距離, 我們要讓所有距離都大於或等於這個距離.
實現方面就是, 我們要怎麼算這個用了多少次移動機會, 還有怎麼去移動呢?
我們發現他所給的石頭距離是一個前綴和. 全部都是從起點到某個石頭的距離, 而且還是順序給出.
這樣的話, 我們就可以用O(1)的時候直接把兩個點相減直接算到兩點的距離, 假設你把兩點中間的點都刪掉了, 是不會影響從a 點 到b 點的總距離的.只會影響最短距離.
所以直接一個for 循環, 只要發現最後一次記錄的點 和當前點匹配的時候發現距離少於二分枚舉出來的點, 就直接用一個counter 計算移動次數 + 1. 如果比二分枚舉的x 要大 就直接跟新最後一詞出現的點.
- #include<iostream>
- using namespace std;
- int dis, n, m, a[1000000];
- bool check(int x){
- int lastt = a[0];
- int cnt = 0;
- for(int i = 1; i <= n; i++){
- if(a[i] - lastt < x){
- cnt++;
- }else{
- lastt = a[i];
- }
- }
- return cnt <= m && cnt <= n;
- }
- void work(){
- a[0] =0;
- cin >> dis >> n >> m;
- for(int i = 1; i <= n; i++){
- int y;
- cin >> a[i];
- }
- int l = 0, r = dis;
- int ans = 0;
- while(l <= r){
- int mid = (l + r) / 2;
- if(check(mid)){
- l = mid + 1;
- ans = mid;
- }else{
- r = mid - 1;
- }
- }
- cout << ans << endl;
- }
- int main(){
- work();
- return 0;
- }
我們發現如果第i頭牛不能拿到禮物,他後面的所有牛都不可能拿到禮物.
這裡具有11110000 這樣的單調性. 所以可以利用二分進行搜索最後一個出現可行的位置.
也就是我們二分出來的x 是可以拿到禮物的牛的總數量.
如何判斷是不是合法呢?
我們發現如果在從0 到i 這個位置之間, 如果有多于i頭牛的數量, 那麼就會出現無限循環了, 有牛出隊, 再從新進隊, 並且位置就在原來的位置.
最簡單的檢查方法就是, 我們直接用一個sum 記錄 在x 之前多少頭牛. 假設 我們已經知道每頭牛將會排在哪個位置, 我們就把0 到 x 的每個位置 會出現的牛的數量加起來, 如果超過他們所在的索引, 那麼就是犯法的. 原因是 在 x 前 你要讓他的和大於x, 只有當某個牛他會進隊的位置剛剛好在x 之前, 那麼就很明顯, 會開始一個循環.
由於牛所排的位置, 是從尾巴數過來的, 所以我們要提前做一個處理, 把牛會排的位置變成從頭數起會排在哪.
- #include<iostream>
- #include<cstring>
- using namespace std;
- int n, a[100000];
- bool check(int mid){
- int s[1000000];
- memset(s, 0, sizeof(s));
- for(int i = 1; i< mid; i++){
- s[a[i]]++;
- }
- int sum = 0;
- for(int i = 1; i < mid; i++){
- sum+= s[i];
- if(sum >= i)return 1;
- }
- return 0;
- }
- int main(){
- cin >> n;
- for(int i = 1; i <= n; i++){
- cin >> a[i];
- a[i] = n - a[i];
- }
- int l =0, r = n;
- while(l < r){
- int mid = l + r + 1>> 1;
- if(check(mid)){
- r = mid - 1;
- }else{
- l = mid;
- }
- }
- cout << n - r << endl;
- return 0;
- }
由於高於某個長度之後,的答案都是不可能的, 就是11110000 二分.
所以我們直接二分可能的長度, 然後看看到底能不能切出指定數量 k.
- #include<iostream>
- using namespace std;
- int n, k;
- int w[300000];
- bool check(int mid){
- int cnt = 0;
- for(int i = 1; i <= n; i++){
- cnt += w[i]/mid;
- }
- return cnt >= k;
- }
- int main(){
- cin >> n >> k;
- for(int i = 1; i <= n; i++){
- cin >> w[i];
- }
- int l = 1, r = 2e9;
- while(l <= r){
- long long mid = l + r >> 1;
- if(check(mid)){
- l = mid + 1;
- }else{
- r = mid - 1;
- }
- }
- cout << l - 1<< endl;
- return 0;
- }
题目的公式是跟你说, Y = 某一个区间中, 只计算 那些 重量比 你所选的重量W 要重的矿产的价值和.
价值和的公式 为 累积的数量 * 累积的价值.
所以我们需要有数量前缀和, 和 价值前缀和, 并且 这些前缀和都是把 重量 比W小的剔除了的.
然后我们二分 W.
这里你会发现 当W 越小, 符合条件的矿产越多, 计算出来的价值和会越大, 所以当你发现价值和比S大了, 你就把 W 变大, 反之亦然.
- #include<bits/stdc++.h>
- #include<cstring>
- using namespace std;
- long long n, m, s;
- long long w[300000], v[300000];
- long long cnt[300000], sum[3000000];
- struct node{
- int l, r;
- }a[3000000];
- long long check(int mid){
- //cal the prefix sum and cnt
-
- for(int i = 1; i <= n; i++){
- if(w[i] >= mid){
- cnt[i] = cnt[i - 1] + 1;
- sum[i] = sum[i - 1] + v[i];
- }else{
- cnt[i] = cnt[i - 1];
- sum[i] = sum[i - 1];
- }
- }
- long long res = 0;
- for(int i = 1; i <= m; i++){
- res += (cnt[a[i].r] - cnt[a[i].l - 1])*(sum[a[i].r] - sum[a[i].l - 1]);
- }
- return res;
- }
- int main(){
- cin >> n >> m >> s;
- for(int i = 1; i <= n; i++){
- cin >> w[i] >> v[i];
- }
- for(int i = 1; i <= m; i++){
- cin >> a[i].l >> a[i].r;
- }
- int l = 1, r = 1e9;
- int ans = 0;
- while(l <= r){
- int mid = l + r >> 1;
- if(check(mid) >= s){
- l = mid + 1;
- }else{
- r = mid - 1;
- }
- }
- cout << min(abs(check(r) - s), abs(check(r + 1) - s))<< endl;
- return 0;
- }