CUSTACM Summer Camp 2022 Training 6(10题)

本文介绍了四个涉及二分法的算法题目,包括数组加法操作达到所有数字相同、矩阵平移、邀请朋友吃饭和序列子段求解。通过分析题意和利用二分查找的特性,分别提出解决方案,涉及的复杂度从O(n)到O(n log n)不等。此外,还涵盖了线段树、双指针、区间DP等算法思想。
摘要由CSDN通过智能技术生成

CUSTACM Summer Camp 2022 Training 6

A. Water the Trees

题意

一个数组通过加法操作,最终让所有数字都相同。奇数天+1,偶数天+2(可以选任意数字去+1、+2),问最少多少天能达到目标。(可以跳过某一天)

数据范围: 1 ≤ n ≤ 3 ∗ 1 0 5 1 \leq n \leq 3*10^5 1n3105

tags:二分,思维

思路

最开始的思路是让它三步一走三步一走(防止出现跳过的情况),最后在判断需要的+1和+2次数,但是最后的时候会出现很多跳过的情况,不是正确解法

  1. 若mid天可以达成目的,那么天数大于mid也可达成目的;若mid天不能达成目的,那么天数小于mid也不能达成目的。满足单调性,可以用二分来搜索天数
  2. 对于一个天数mid如何确定齐是否可以达成目的?
    1. 对于mid天,我们可以实现mid/2+mid%2次+1,mid/2次+2
    2. 最终让所有数字相同,这个数字一定是数组中最大的那个(**或+1!**后面解释),设为max
    3. 让其他数字ai与max比较奇偶性
      1. 若同奇同偶,那么只需将ai不断+2使其到max(+2可以是+1+1也可以是+2
      2. 若非同奇偶性,先+1使其同奇偶,在执行上面那一步
    4. 将所有数字变为与max同奇偶性后,只需将所有剩余步骤的总效果相加看看是否增加的高度是否可以弥补所有数的高度差(注意总效果要看+1的剩余次数是奇数还是偶数,如果为奇数则+1的效果要-1,因为能改变奇偶性了)
    5. 最大高度可能要+1,原因是改变奇偶性,是使原来同奇同偶变为非同奇同偶,这个最终结果可能更优

代码

#include<iostream>
#include<algorithm>
#define ll long long
using namespace std;

const int maxn=3e5+5;
int n;
ll a[maxn];

bool judge(ll x){
    sort(a,a+n);
    ll h1=a[n-1],h2=a[n-1]+1;//两种最大高度
    ll haveji=x/2+x%2,haveou=x/2;//+1次数与+2次数
    ll sum1=0,sum2=0,dif1=0,dif2=0;
    for(int i=0;i<n-1;i++){
        sum1+=h1-a[i];
        sum2+=h2-a[i];
        if((a[i]&1)!=(h1&1))dif1++;//非同奇同偶
        else dif2++;//另一种高度的非同奇同偶
    }
    if((haveji>=dif1)&&(haveou*2+(haveji-dif1)/2*2>=sum1-dif1))return true;//判断剩余的+1与+2次数是否可以达成目的
    if((haveji-1>=dif2)&&(haveou*2+(haveji-1-dif2)/2*2>=sum2-dif2))return true;
    // cout<<haveji<<' '<<haveou<<' '<<dif2<<' '<<sum2<<endl;
    return false;
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int t;
    for(cin>>t;t;t--){
        cin>>n;
        for(int i=0;i<n;i++)cin>>a[i];
        judge(9);
        ll l=0,r=1e15;
        while(l<=r){//二分,注意边界条件
            ll mid=(l+r)/2;
            if(judge(mid))r=mid-1;
            else l=mid+1; 
        }
        cout<<l<<endl;
    }
}

复杂度

O ( n ∗ l o g 1 e 15 ) O(n*log1e15) O(nlog1e15)


B. Matrix and Shifts

题意

给出一个 n * n 的 01 矩阵(矩阵元素只有 0 或 1),可以进行任意次的任意平移操作(每行向上平移、每行向下平移、每列向左平移、每列向右平移)(如向下平移第n行移到第1行),这个操作无消耗;还可以进行单点修改操作,即将 0 改为 1 或将 1 改为 0,每次消耗 1 burl,求将矩阵变为对角线全为1,其余全为0

数据大小: 1 ≤ n ≤ 2000 1 \leq n \leq 2000 1n2000

tags:思维

思路

平移矩阵使1尽可能的多的分布在对角线上(有count个),则对角线上0有n-count个,其他地方1有num1-count个(num1为1的总数),最终结果就为num1-count+n-count

对于平移,可以把它看成 n ∗ ( 2 n − 1 ) n*(2n-1) n(2n1)的矩阵,该矩阵包含上述上下左右平移时所有对角线的情况

在这里插入图片描述

为了方便遍历可以多次赋值原矩阵形成 n ∗ 2 n n*2n n2n(或者 2 n ∗ 2 n 2n*2n 2n2n)的矩阵,然后暴力遍历对角线

但是可以用余数来避免赋值

代码

#include<iostream>
using namespace std;

const int maxn=2010;
char a[maxn][maxn];

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int t;
    for(cin>>t;t;t--){
        int n;
        cin>>n;
        for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
        cin>>a[i][j];
        int num1=0;
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
                if(a[i][j]=='1')num1++;
            }
        }
        int count=0,mx=0;
        for(int i=1;i<=n;i++){//只用考虑上下平移即可,包含了左右平移中对角线的情况
            count=0;
            for(int j=i,x=1;x<=n;x++,j++){
                if(j>n)j=1;//j到了n+1,其实是到了(n+1)%n=1
                if(a[j][x]=='1')count++;
            }
            mx=max(count,mx);
        }
        cout<<n-mx+num1-mx<<endl;
    }
}

复杂度

O ( n 2 ) O(n^2) O(n2)


C. Keshi Is Throwing a Party二分好题

题意

你有n个朋友,第i个朋友有i元钱,选一些朋友来吃饭,对于第i个朋友,比他richer的超过ai个,比他poorer的不超过bi个,问最多可邀请多少个朋友来

数据大小: 1 ≤ n ≤ 2 ∗ 1 0 5 1 \leq n \leq 2*10^5 1n2105

tags:二分,思维

思路

若可以邀请mid个朋友,那么必然可以邀请小于mid个朋友;若不能邀请mid个朋友,那么必然不能邀请大于mid个朋友。满足单调性,可以用二分

判断是否可以邀请mid个朋友

  1. 从小到大开始一个个邀请,假设已经邀请了cnt个人,判断第i个朋友是否可以被邀请、
  2. 这cnt个人一定比他穷,要求 c n t ≤ b i cnt \leq b_i cntbi
  3. 未邀请的mid-cnt人都比它富,要求 m i d − c n t ≤ a i mid-cnt \leq a_i midcntai
  4. 若可以被邀请,则cnt++,若最终cnt>=x则可以邀请mid个朋友

代码

#include<iostream>
using namespace std;

const int maxn=2e5+5;
int n;
int a[maxn],b[maxn];

bool judge(int x){
    int cnt=0;
    for(int i=1;i<=n;i++){
        if(x-cnt-1<=a[i]&&cnt<=b[i])cnt++;
    }
    return cnt>=x;
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int t;
    for(cin>>t;t;t--){
        cin>>n;
        for(int i=1;i<=n;i++)cin>>a[i]>>b[i];
        int l=0,r=n;
        while(l<=r){
            int mid=(l+r)/2;
            if(judge(mid))l=mid+1;
            else r=mid-1;
        }
        cout<<r<<endl;
    }
}

复杂度

O ( n ∗ l o g n ) O(n*logn) O(nlogn)


D. ATM and Students

题意

给定序列,求出最长的子段,满足其每个前缀和+s都大于等于0,s是给定的常数。

数据范围: 1 ≤ n ≤ 2 ∗ 1 0 5 , 0 ≤ s ≤ 1 0 9 , − 1 0 9 ≤ a i ≤ 1 0 9 1 \leq n \leq 2*10^5,0 \leq s \leq 10^9,-10^9 \leq a_i \leq 10^9 1n2105,0s109,109ai109

tags:双指针

思路

两个指针l、r,代表字段的the first的序号和the last的序号,若l到r的和>=0推进r,否则推进l。其间不断更新最大长度的字段

注意:会出现 l > r l >r l>r的情况,如序列开头为-20且s<20。这时sum-a[l],sum+a[r]来越过这个点实现l=r。

代码

#include<iostream>
#define ll long long
using namespace std;

const int maxn=2e5+5;
int n;
ll s;
ll a[maxn];

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int t;
    for(cin>>t;t;t--){
        cin>>n>>s;
        for(int i=1;i<=n;i++)cin>>a[i];
        int l=1,r=1,ml=-1,mr=-1,mx=0;
        ll sum=s;
        while(l<=n&&r<=n){
            while(r<=n&&sum+a[r]>=0){//注意判断当前r所在位置
                sum+=a[r];
                r++;
            }
            // cout<<l<<' '<<r<<endl;
            if(r-l>mx){//注意区间长度
                mx=r-l;
                ml=l,mr=r-1;//右端点不是r,而是r-1,因为sum+a[r]<0
            }
            sum-=a[l++];
        }
        if(ml==-1)cout<<-1<<endl;
        else cout<<ml<<' '<<mr<<endl;
    }
}

复杂度

O ( n ) O(n) O(n)


E. Integers Have Friends线段树好题

题意

给你一个长度为n的序列,要求最长的序列(连续的),且满足存在一个数m( m ≥ 2 m\geq 2 m2)每个元素%m都相等,

数据范围: 1 ≤ n ≤ 2 ∗ 1 0 5 , 1 ≤ a i ≤ 1 0 18 1 \leq n \leq 2*10^5,1 \leq a_i \leq 10^{18} 1n2105,1ai1018

tags: 线段树,思维,双指针

思路

每一个数mod m都相等,那么该序列的每个相邻差ai+1–ai都是m的倍数,即所有相邻差的gcd至少为m,即gcd>1( m > 1 m>1 m>1

  1. 先建立一个差分数组
  2. 和D题一样用双指针来找最长序列,该序列的gcd>1
    1. (该序列可能会出现l>r的情况,因为如果序列长度为1,则gcd==1,l推进使l>r)
    2. 注意:当l>r时,我写的线段数方法会出现死循环,一直执行query(2*k,l,r),原因我的线段树是对于l<=r的情况,而l=r时会return,但是出现l>r时不会return,而会一直执行return gcd(query(2*k,l,mid),query(2*k+1,mid+1,r));此bug找了1多小时了啊啊啊啊,所以要特判一下 if(l>r)return 2;,且不能return <=1的数,不然双指针部分又死循环。反正细节是真的多
    3. 而对于查找一个区间的gcd,可以用线段树来维护区间的gcd
    4. 特判:当n=1时,cout<<1(差分数组长度为0,建树和双指针都进行不了)

代码

#include<iostream>
#include<cmath>
#define ll long long
using namespace std;

const int maxn=2e5+5;
int n;
ll num[maxn];

ll gcd(ll a,ll b){
    return b==0?a:gcd(b,a%b);
}

struct node{
    int l,r;
    ll g;
}a[4*maxn];

void update(int k){
    a[k].g=gcd(a[2*k].g,a[2*k+1].g);
}

void build(int k,int l,int r){
    a[k].l=l,a[k].r=r;
    if(l==r){
        a[k].g=num[l];
        return;
    }
    int mid=(l+r)>>1;
    build(2*k,l,mid);
    build(2*k+1,mid+1,r);
    update(k);
}

ll query(int k,int l,int r){
    // cout<<k<<' '<<l<<' '<<r<<endl;
    if(l>r)return 2;//注意特判l>r的情况,并返回>1的值
    if(l==a[k].l&&r==a[k].r)return a[k].g;
    int mid=(a[k].l+a[k].r)>>1;
    if(r<=mid)return query(2*k,l,r);
    else if(l>mid)return query(2*k+1,l,r);
    else return gcd(query(2*k,l,mid),query(2*k+1,mid+1,r));
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int t;
    cin>>t;
    while(t--){
        cin>>n;
        for(int i=1;i<=n;i++)cin>>num[i];
        for(int i=1;i<=n-1;i++)num[i]=abs(num[i+1]-num[i]);//差分数组,记得加绝对值
        if(n==1){//特判
            cout<<1<<endl;
            continue;
        }
        build(1,1,n-1);
        int l=1,r=1,mx=0;
        while(1){//双指针
            // cout<<l<<' '<<r<<endl;
            while(r<=n-1&&query(1,l,r)>1){
                r++;
            }
            mx=max(mx,r-l);
            if(r>=n)break;
            while(l<=r&&query(1,l,r)==1)l++;
        }

        cout<<mx+1<<endl;
    }
}

复杂度

O ( n ∗ l o g n ) O(n*logn) O(nlogn)


F. The Sports Festival区间dp不熟悉

题意

给定一个长度为n的序列,构造一个序列使得$ \sum_{i = 1}^{n} d_i$最小。
其中 d i = m a x ( a 1 , a 2 , . . . . . , a i ) − m i n ( a 1 , a 2 , . . . . , a i ) d_i=max(a_1,a_2,.....,a_i)-min(a_1,a_2,....,a_i) di=max(a1,a2,.....,ai)min(a1,a2,....,ai)

数据范围: 1 ≤ n ≤ 2000 , 1 ≤ s i ≤ 1 0 9 1 \leq n \leq 2000,1 \leq s_i \leq 10^9 1n2000,1si109

tags:区间dp

思路

对于区间1到n的最优解的序列,最后一个数一定是min或max,因为若min或max在前面出现会使得序列会变得更大

那么对于区间1到n的最优解,是在区间1到n-1的最优解序列最后加上min或max

相对应的,长度为n-1的区间包含max或min,这样P[n]=P[n-1]+max-min

于是,在dp中我们就要讨论在长度为n-1的最优解区间中,是包含min还是包含max

先对区间排序,方便寻找最大值和最小值

  1. DP的抽象dp[i][j]:从i到j的序列中的最优序列的最小和 ∑ i = l r d i \sum_{i=l}^{r}d_i i=lrdi
  2. 状态转移方程dp[i][j]=min(dp[i+1][j],dp[i][j-1])+a[j]-a[i],(因为我们已经对a数组排序了,所以a[j]-a[i]就是max-min,而dp[i+1][j]表示包含max区间,dp[i][j-1]表示包含min的区间)
  3. 边界dp[i][i]=0

代码

#include <iostream>
#include <algorithm>
#define ll long long
using namespace std;

const int maxn = 2005;
int n;
int a[maxn];
ll dp[maxn][maxn];

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    sort(a + 1, a + 1 + n);
    for (int len = 2; len <= n; len++)
    {
        for (int i = 1; i <= n - len + 1; i++)
        {
            dp[i][i + len - 1] = min(dp[i + 1][i + len - 1], dp[i][i + len - 2]) + a[i + len - 1] - a[i];
            // cout<<dp[i][i+len-1]<<' ';
        }
        // cout<<endl;
    }

    cout << dp[1][n] << endl;
}

复杂度

O ( n 2 ) O(n^2) O(n2)


G. DMCA

题意

求数根

tags:数论

思路

有公式,当然也可以按正常方法模拟,多轮将每位数相加,直到其<10为止

公式 n 的数根 = ( n − 1 ) m o d    9 + 1 n的数根=(n-1)\mod 9 +1 n的数根=(n1)mod9+1

代码

#include<iostream>
using namespace std;

int main(){
    int n;
    cin>>n;
    cout<<(n-1)%9+1<<endl;
}

复杂度

O ( 1 ) O(1) O(1)


H. Rock, Paper, Scissors

题意

两个人A、B玩n轮石头剪子布,各有a1,a2,a3(和为n)、b1,b2,b3(和为n)个石头剪子布,求A最少可以嬴多少轮,最多可以嬴多少轮

数据范围: 1 ≤ n ≤ 1 0 9 1 \leq n\leq 10^9 1n109

tags:思维

思路

  • 最多可以嬴多少轮

    直接让A的石头全部对B的剪刀、A的剪刀全部对B的布、A的布全部对B的石头。取其中的最小值相加即可。

  • 最少可以嬴多少轮

    最开始的思路是让B先尽可能去嬴A,在然后尽可能去打平局,剩下的两人一定会只有一种选择了,且是A嬴B的情况,但是这样会因为比如二者最后是A剩布、B石头,如果先前用B的布去嬴A的石头,会导致A的石头比较少而不能去和B的石头打平局(打平局的目的是消耗B的石头,因为最后多剩石头要让石头尽可能少)

    所以重新想一下正确思路应该为:假设最后的情况为A只有石头,B只有剪刀;A只有剪刀,B只有布;A只有布,B只有石头的三种情况中的一种,然后去尽可能让B嬴A和打平来消耗它们最后剩的那个选择,比较三种情况的最小值

    但是注意:三种情况中可能会出现负数的情况,代表那种情况不会发生(事实上,上述三种情况中只会发生一种,也就是会出现两种情况为负数的情况,下面选择比较最大值来舍弃另外两个为负数的情况),这时后求最后结果舍弃那种情况了

代码

#include<iostream>
using namespace std;

int main(){
    int n;
    cin>>n;
    int a1,a2,a3,b1,b2,b3;
    cin>>a1>>a2>>a3;
    cin>>b1>>b2>>b3;
    int mx=min(a1,b2)+min(a2,b3)+min(a3,b1);
    int x=max(0,b1-a1-a2);//尽可能消耗B的石头
    int y=max(0,b2-a2-a3);//尽可能消耗B的剪刀
    int z=max(0,b3-a3-a1);//尽可能消耗B的布
    int mn=max(max(x,y),z);//求max,舍弃为两个负数
    cout<<mn<<' '<<mx<<endl;
}

复杂度

O ( 1 ) O(1) O(1)


I. RPG Protagonist

题意

有两人的容量分别为p,f,各有cnts、cntw件重量为s、w的剑和战斧,问两人共同最多可以带多少件物品

数据范围: 1 ≤ p , f ≤ 1 0 9 , 1 ≤ c n t s , c n t w ≤ 2 ∗ 1 0 5 , 1 ≤ s , w ≤ 1 0 9 1 \leq p,f \leq 10^9,1 \leq cnt_s,cnt_w \leq 2*10^5,1\leq s,w \leq 10^9 1p,f109,1cnts,cntw2105,1s,w109

tags:折半枚举思想

思路

类似折半枚举,只是这道题不用我们去折半,只有去枚举即可

假设剑比较轻,枚举A带0、1、2……件剑、然后判断A还可以带多少战斧;在让B尽可能带多的剑(因为剑轻),然后在判断其可以带多少战斧。其间注意剑和战斧的数量上限

代码

#include<iostream>
using namespace std;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int t;
    for(cin>>t;t;t--){
        int p,f,cnts,cntw,s,w;
        cin>>p>>f>>cnts>>cntw>>s>>w;
        if(s>w){//如果剑重于战斧,交换一下即可
            int t=s;
            s=w;w=t;
            t=cnts;
            cnts=cntw,cntw=t;
        }
        // cout<<p<<' '<<f<<' '<<cnts<<' '<<cntw<<' '<<s<<' '<<w<<endl;
        int mx=0;
        for(int i=0;i*s<=p&&i<=cnts;i++){
            int a1=i;
            int a2=min(cntw,(p-a1*s)/w);
            int lefts=cnts-i,leftw=cntw-a2;
            int b1=min(lefts,f/s);
            int b2;
            if(f-b1*s>=0)b2=min(leftw,(f-b1*s)/w);
            mx=max(mx,a1+a2+b1+b2);
            // cout<<a1<<' '<<a2<<' '<<b1<<' '<<b2<<endl;
        }
        cout<<mx<<endl;
    }
}

复杂度

O ( p ) O(p) O(p)


J. Array Destruction数据结构set好题

题意

2n个数,初始指定一个x,然后选两个和为x的数,删掉这两个数,再把x替换为两个数的最大值。
问能否删掉所有数,并输出删数的过程。

数据范围: 1 ≤ 1000 ≤ n 1 \leq 1000 \leq n 11000n

tags:思维

思路

一定是从最大的数开始删,如果从最大的数开始删,那么这个数将永远无法删除

第一次删除,需要来选一个数来与最大的数max匹配,其和为x,删除这两数,x=max(不用担心复杂度,因为可以用n2了都)

然后选当前最大数 the second max+?=max,我们就需要一个数据结构:可以得到当前最大值、可以查找数、并且时刻更新序列(删除这两数)→set(multiset)

细节:

  1. 最大的数为set的最后一个数,但是不可以用s.end()-1来指向它,必须先用auto it=s.end()、再it–才能正确指向它,或者用函数prev(s.end())
  2. 查找当前序列的最大值temp并删除,可以用s.erase(s.find(temp)),但要保证temp一定存在,不然find返回s.end()会导致erase出错,相当于越界了

代码

#include<iostream>
#include<set>
#include<vector>
#include<algorithm>
#include<utility>
using namespace std;

typedef pair<int,int>P;
const int maxn=1005;
int n;
int a[2*maxn];
multiset<int>s;
vector<P>v;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int t;
    for(cin>>t;t;t--){
        cin>>n;
        for(int i=1;i<=2*n;i++)cin>>a[i];
        sort(a+1,a+1+2*n);
        for(int i=1;i<=2*n-1;i++){
            s.clear();
            v.clear();
            for(int j=1;j<=2*n;j++)s.insert(a[j]);
            s.erase(s.find(a[2*n]));
            s.erase(s.find(a[i]));
            v.push_back(P(a[i],a[2*n]));
            int all=a[2*n];
            for(int j=1;j<=n-1;j++){
                int temp=*prev(s.end());
                s.erase(s.find(temp));//temp要在这里删除
                if(s.find(all-temp)!=s.end()){//若all-temp存在
                    s.erase(s.find(all-temp));//删除all-temp
                    //s.erase(s.find(temp));不能在这里删除,因为虽然可以保证temp存在,但是可能all-temp=temp,可以会在上一步把temp删除了,这时它就不存在了
                    v.push_back(P(all-temp,temp));
                    all=temp;
                }
                else break;
            }
            if(v.size()==n)break;
        }
        if(v.size()!=n)cout<<"NO"<<endl;
        else{
            cout<<"YES"<<endl;
            cout<<v[0].first+v[0].second<<endl;
            for(int i=0;i<v.size();i++){
                cout<<v[i].first<<' '<<v[i].second<<endl;
            }
        }
    }
}

复杂度

O ( n 2 ) O(n^2) O(n2)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值