CUSTACM Summer Camp 2022 Training 8(8题)题解

CUSTACM Summer Camp 2022 Training 8

无AI题解,以前写过

B. The Robot

题意

给你一个字符串表示的路径,其中LRUD分别表示x–,x++,y++,y–,要你在地图上一个位置放一个障碍物,使机器人从(0,0)按路径走后会返回起点。若无法实现输出(0,0);

数据大小: 1 ≤ 字符串长度 ≤ 5000 1 \leq 字符串长度 \leq 5000 1字符串长度5000

tags:暴力

思路

数据范围很小,可以直接使用暴力,对路径上每一个点放一个障碍物看看它会不会返回起点

不要总觉得暴力不行直接pass,看好数据范围估计暴力复杂度

代码

#include<bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int t;
    for(cin>>t;t;t--){
        string s;
        cin>>s;
        vector<pair<int,int>>v;
        int x=0,y=0;
        for(int i=0;i<s.length();i++){
            if(s[i]=='D')y--;
            else if(s[i]=='U')y++;
            else if(s[i]=='L')x--;
            else if(s[i]=='R')x++;
            v.push_back(make_pair(x,y));
        }
        bool judge=false;
        for(int i=0;i<v.size();i++){
            int x=0,y=0;
            for(int j=0;j<s.length();j++){
                if(s[j]=='D')y--;
                else if(s[j]=='U')y++;
                else if(s[j]=='L')x--;
                else if(s[j]=='R')x++;
                if(x==v[i].first&&y==v[i].second){
                    if(s[j]=='D')y++;
                    else if(s[j]=='U')y--;
                    else if(s[j]=='L')x++;
                    else if(s[j]=='R')x--;
                }
            }
            if(x==0&&y==0){
                cout<<v[i].first<<' '<<v[i].second<<endl;
                judge=true;
                break;
            }
        }
        if(!judge)cout<<0<<' '<<0<<endl;
    }
}

复杂度

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


C. Divide and Summarize线段树好题

题意

一个长度为n的数组,可以进行以下操作:令mid=(max+min)/2,让<=mid的数在数组左侧,让>mid的数在数组右侧,可以选择丢弃一侧数组,将另一侧数组代替原数组

有q次询问,是否可以进行上述操作得到一个和为s的数组

数据大小: 1 ≤ n , q ≤ 1 0 5 , 1 ≤ a i ≤ 1 0 6 1 \leq n ,q \leq 10^5,1 \leq a_i \leq 10^6 1n,q105,1ai106

tags:思维,二分,线段树

思路

其实上述过程就是建立线段树的过程,只不过有几处不同

  1. 对于普通线段树建立int mid=(l+r)/2,但是对于本题,以(max+min)/2的位置作为mid,可以用二分找
  2. 对于普通线段树的return条件if(l==r),但是对于本题,如果有l到r的值都相等就return,a[k]的值为该区间和,可以先维护以下前缀和
  3. 在建树的过程中把a[k].sum用map标记一下,之和q次询问都可以在O(1)时间内求出

细节:

  1. 开longlong

  2. 不用去for循环判断是否整个区间都相等,因为区间是有序的,只需判断a[l]是否等于a[r]就行

  3. 普通线段树大小为 4 ∗ n 4*n 4n即可,但是这题的线段树不是每次一半一半分的,我开到100倍大小才过。唉,比赛中就要自闭了

  4. 其实因为本题不需要区间查找、修改等操作,把线段树存起来没用作用还占空间,所以可以只对建树过程中的区间和用map标记一下就行,这样就不用怕上面溢出问题了

在这里插入图片描述

代码

最初代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;

const int maxn=1e5+5;
int n,q;
ll num[maxn],b[maxn];
struct node{
    ll l,r,sum;
    node(){
        l=r=sum=0;
    }
}a[100*maxn];
map<ll,int>m;

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

void build(int k,int l,int r){
    a[k].l=l,a[k].r=r;
    if(l==r||num[l]==num[r]){
        a[k].sum=b[r]-b[l-1];
        m[a[k].sum]++;
        return;
    }
    // int mid=(l+r)/2;
    int x=(num[l]+num[r])/2;
    int mid=upper_bound(num+1,num+n+1,x)-num-1;
    build(2*k,l,mid);
    build(2*k+1,mid+1,r);
    update(k);
    m[a[k].sum]++;
}

int main(){
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    int t;
    for(cin>>t;t;t--){
        cin>>n>>q;
        // for(int i=0;i<10*n;i++)a[i].l=0,a[i].r=0,a[i].sum=0;
        for(int i=0;i<=n;i++)b[i]=0;
        m.clear();
        for(int i=1;i<=n;i++)cin>>num[i];
        sort(num+1,num+n+1);
        for(int i=1;i<=n;i++)b[i]=num[i]+b[i-1];
        build(1,1,n);
        while(q--){
            int x;
            cin>>x;
            if(m[x])cout<<"YES"<<endl;
            else cout<<"NO"<<endl;
        }
    }
}
改进代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;

const int maxn=1e5+5;
int n,q;
ll num[maxn],b[maxn];
map<ll,int>m;


void build(int l,int r){
    m[b[r]-b[l-1]]++;
    if(l==r||num[l]==num[r])return;
    int x=(num[l]+num[r])/2;
    int mid=upper_bound(num+1,num+1+n,x)-num-1;
    build(l,mid);
    build(mid+1,r);
}

int main(){
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    int t;
    for(cin>>t;t;t--){
        cin>>n>>q;
        for(int i=0;i<=n;i++)b[i]=0;
        m.clear();
        for(int i=1;i<=n;i++)cin>>num[i];
        sort(num+1,num+n+1);
        for(int i=1;i<=n;i++)b[i]=num[i]+b[i-1];
        build(1,n);
        while(q--){
            int x;
            cin>>x;
            if(m[x])cout<<"YES"<<endl;
            else cout<<"NO"<<endl;
        }
    }
}

D. Row GCD

题意

给定一个长度为n的序列a[],和长度为m的序列b[],对于每个j,求每个a[i],加上b[j]后的gcd(a[0]…a[n])

tsgs:辗转相除法,更相减损法

思路

对于每个a[0…n]加上b[j]后的gcd,可以对后面的a[1…n]项分别减去b[j](更具更相减损法此处gcd不变),这样a[1…n]的gcd就是原来没有加上b[j]的gcd了,先用辗转相除法预先求出该部分gcd,然后于a[0]+b[j]求gcd就行

代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;

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

ll gcd(ll x,ll y){
    return y==0?x:gcd(y,x%y);
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>m;
    for(int i=0;i<n;i++)cin>>a[i];
    for(int j=0;j<m;j++)cin>>b[j];
    for(int i=1;i<n;i++)a[i]=abs(a[i]-a[0]);
    ll x=0;
    for(int i=1;i<n;i++)x=gcd(a[i],x);
    for(int j=0;j<m;j++){
        cout<<gcd(b[j]+a[0],x)<<' ';
    }
}

E. Rating Compression思维好题

题意

给你长度为n的数组,对于每个k从1到n,有n-k+1个数组的连续子集,其中判断取每个子集的最小值是否可以构成长度为n-k+1的排列

数据大小: 1 ≤ n ≤ 3 ∗ 1 0 5 , 1 ≤ a i ≤ n 1 \leq n \leq 3*10^5,1 \leq a_i \leq n 1n3105,1ain

tags:思维

思路

  1. 对于一个较小的数,它只能处在数组两端,不然会出现多个子集的最小值为它

    以1为例,当k=n-1时,要把数组分成两个子集,那么1只能在数组最左或最右段,不然的话两个子集的最小值都为1

    承接上面,以2为例,当k=n-2时,要把数组分成三个子集,那么2只能在中间那个子集的两端出现,不然的话有两个子集的最小值都为2

    以此类推

    于是逆向遍历,一次划分的子集个数为2个、3个…n-1个,每次把最小值放在队列两端,直到有不满足题目要求的就break,因为后面肯定也会不满足要求

  2. 对于k=n或k=1要单独判断,因为它们不用符合上面最小值在队列两边的要求

代码

#include<bits/stdc++.h>
using namespace std;

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

int main(){
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    int t;
    for(cin>>t;t;t--){
        cin>>n;
        map<int,int>m;
        for(int i=0;i<=n;i++)out[i]=0;
        for(int i=0;i<n;i++){
            cin>>a[i];
            m[a[i]]++;
        }
        bool judge=true;
        for(int i=1;i<=n;i++)if(m[i]!=1)judge=false;
        if(judge)out[1]=1;//特判k=1
        if(m[1])out[n]=1;//特判k=n
        int l=0,r=n-1,index=1;//左右指针为0,n-1。index表示当前应该在队列端的最小值
        for(int i=n-1;i>=1;i--){
            if(m[index+1]&&m[index]==1&&(a[l]==index||a[r]==index)){//若满足index只有一个(如果有多个那么它会在中间出现,肯定不满足),并且有index+1出现,而且index在队列两端,那么符合要求
                out[i]=1;//k=i就符合要求,注意时逆向遍历,子集个数越来越大,这样才能把最小值依次放两边
                if(a[l]==index)l++;//更新队列两端
                else r--;
                index++;
            }
            else break;//有不符合条件的,后面的肯定也不会符合条件
        }
        for(int i=1;i<=n;i++)cout<<out[i];
        cout<<endl;
    }
}

F. Zigzags

题意

给你一个数组,找出四元向量的数量(i,j,k,l)

  1. 1 ≤ i < j < k < l ≤ n 1\leq i<j<k<l\leq n 1i<j<k<ln
  2. a i = a k 并且 a j = a l a_i=a_k并且a_j=a_l ai=ak并且aj=al

数据大小: 4 ≤ n ≤ 3000 4 \leq n \leq 3000 4n3000

tags:思维

思路

数据没有很大,n2是每问题的,可以暴力(但不是无脑枚举四个元素)

  1. 确定其中一个元素k,枚举k
  2. 对于出现在k后面的数字,维护其出现的次数
  3. 在k之前寻找与之匹配的数,即寻找i
  4. 在寻找i的过程中维护一下方案数,即sum+在第二步中统计的次数(ik直接的数)
  5. 注意开longlong

代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;

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

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];
        map<int,int>m;
        ll ans=0;
        for(int i=2;i<n;i++){
            m.clear();
            for(int j=i+1;j<n;j++)m[a[j]]++;
            ll sum=0;
            for(int k=i-1;k>=0;k--){
                if(a[k]==a[i])ans+=sum;
                sum+=m[a[k]];
            }
        }
        cout<<ans<<endl;
    }
}

复杂度

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


G. Array Walk思维好题

题意

给你n个数,你可以走k步,最多可以向左走z步,并且不能连续向左走大于1步。每走到一个位置就加上该位置的值,问值最大可以为多少。

数据范围: 2 ≤ n ≤ 1 0 5 , 1 ≤ k ≤ n − 1 , 0 ≤ z ≤ m i n ( 5 , k ) 2 \leq n \leq 10^5,1 \leq k \leq n-1,0 \leq z \leq min(5,k) 2n105,1kn1,0zmin(5,k)

tags:思维

思路

  1. 因为不能连续向左走大于1步,所以除了一直向左走外,就只能多次左右左右横跳走,于是记录下移走过的位置中相邻两数的最大值mx
  2. 不断向前左走,不断更新mx,以及已走步数的价值sum
  3. 当然每走一步,更新剩余的步数,剩余的步数可以实现**min(z,(剩余步数)/2)**次横条,不断更新最大值``sum+min(z,(剩余步数)/2)*mx`

代码

#include<bits/stdc++.h>
using namespace std;

const int maxn=3e5+5;
int n,k,z;
int a[maxn];

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int t;
    for(cin>>t;t;t--){
        cin>>n>>k>>z;
        for(int i=1;i<=n;i++)cin>>a[i];
        int ans=0,mx=0,sum=0;
        for(int i=1;i<=k+1;i++){
            sum+=a[i];//sum时已经向前走了i-1步的score
            if(i!=n)mx=max(mx,a[i]+a[i+1]);//相邻两数之和最大值为nx,目前正位于第i个位置,已经走了i-1步
            ans=max(ans,sum+min(z,(k-i+1)/2)*mx);//剩下有(k-i+1)步,可以供反复横条(k-i+1)/2次
        }
        cout<<ans<<endl;
    }
}

复杂度

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


H. Pinkie Pie Eats Patty-cakes

题意

给你n个数,输出相同的数的最小间距的最大值

数据范围: 1 ≤ n ≤ 1 0 5 1 \leq n \leq 10^5 1n105

tags:思维、贪心

思路

  1. 最大间距取决于出现次数最多(假设为cnt次)的数字,该数字可能不止一种,设有x种
  2. 那么上述x种数字可以形成长度为x的cnt个隔板
  3. 将剩下的n-cnt*x个数均匀分配到每个隔板之中(如果是多次出现数应该放到不同隔板之中,其间隔一定会>=隔板间隔)
  4. 于是每个隔板的最大间隔为(n-cnt*x)/(cnt-1),在假设隔板本身长度提提供的间隔x-1,答案就是二者之和

代码

#include<bits/stdc++.h>
using namespace std;

const int maxn=1e5+5;
int n;
int cnt[maxn];

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++)cnt[i]=0;
        for(int i=0;i<n;i++){
            int x;cin>>x;
            cnt[x]++;
        }
        sort(cnt,cnt+n+1,greater<int>());
        int x=0;
        for(int i=0;i<n;i++)if(cnt[i]==cnt[0])x++;else break;
        cout<<(n-x*cnt[0])/(cnt[0]-1)+x-1<<endl;
    }
}

复杂度

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


J. Flood Fill区间dp好题

题意

有一行长度为n的方格,每个格子有颜色
对于连续的一块相同颜色的格子称为一个连通块
一开始你可以选择一个起点,每次操作你可以将起点所在位置的整个连通块改变为任意一个颜色
问最少进行多少次操作可以把n个方格都变为相同颜色

数据范围: 1 ≤ n ≤ 5000 1 \leq n \leq 5000 1n5000

tags:区间dp

思路

  1. 因为一开始选定了一个点之后就固定了,只能将连通块逐个向左右扩展,若将区间[l,r]扩展为同色,最终颜色一定位a[l]或a[r]
  2. 对于把一个长度为n的区间变成连通块,其实把长度为len<n的区间变成连通块是其子问题,可以用区间dp来做
  3. 长度为len的区间可以由长度为len-1的区间扩展而来:dp[l][r][0]可以由dp[l+1][r][0/1]而来,dp[l][r][1]可以由dp[l][r-1][0/1]而来

看题解还有最长公共子序列、最长回文子序列的方法

代码

区间DP

#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
using namespace std;

const int maxn=5e3+5;
int n;
int a[maxn];
int dp[maxn][maxn][2];

int main(){
        cin>>n;
        for(int i=0;i<n;i++)cin>>a[i];
        for(int i=0;i<n;i++)
        for(int j=0;j<n;j++)
        for(int k=0;k<2;k++)
        if(i!=j)dp[i][j][k]=inf;
        for(int len=2;len<=n;len++){
            for(int l=0;l+len-1<n;l++){
                int r=l+len-1;
                dp[l][r][0]=min(dp[l][r][0],dp[l+1][r][0]+(a[l]!=a[l+1]));
                dp[l][r][0]=min(dp[l][r][0],dp[l+1][r][1]+(a[l]!=a[r]));
                dp[l][r][1]=min(dp[l][r][1],dp[l][r-1][0]+(a[r]!=a[l]));
                dp[l][r][1]=min(dp[l][r][1],dp[l][r-1][1]+(a[r]!=a[r-1]));
            }
        }
        cout<<min(dp[0][n-1][1],dp[0][n-1][0])<<endl;
}

最长回文子序列

求最长回文子序列的两种遍历方式

#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
using namespace std;

const int maxn=5e3+5;
int n;
int a[maxn];
int dp[maxn][maxn];

int main(){
        cin>>n;
        vector<int>v1;
        for(int i=0;i<n;i++)cin>>a[i];
        for(int i=0;i<n;){
            int c=a[i];
            while(a[i]==c)i++;
            v1.push_back(c);
        }
        int len=v1.size();
        for(int i=len-1;i>=0;i--){//倒序遍历
            dp[i][i]=1;
            for(int j=i+1;j<len;j++){
                if(v1[i]==v1[j])dp[i][j]=dp[i+1][j-1]+2;
                else dp[i][j]=max(dp[i][j-1],dp[i+1][j]);
            }
        }
        cout<<len-dp[0][len-1]/2-1<<endl;//暴力原本需要len-1,减去回文字符串长度/2
}
#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
using namespace std;

const int maxn=5e3+5;
int n;
int a[maxn];
int dp[maxn][maxn];

int main(){
    cin>>n;
    for(int i=0;i<n;i++)cin>>a[i];
    vector<int>v;
    for(int i=0;i<n;){
        int c=a[i];
        while(a[i]==c)i++;
        v.push_back(c);
    }
    int len=v.size();
    for(int i=0;i<n;i++)dp[i][i]=1;
    for(int i=2;i<=len;i++){//斜着遍历(按子序列长度遍历)
        for(int j=0;j+i-1<len;j++){
            if(v[j]==v[j+i-1])dp[j][j+i-1]=dp[j+1][j+i-2]+2;
            else dp[j][j+i-1]=max(dp[j][j+i-2],dp[j+1][j+i-1]);
        }
    }
    cout<<len-1-dp[0][len-1]/2<<endl;
}
最长公共子序列

会超时,因为进行了n次dp,复杂度接近 O ( n 3 ) O(n^3) O(n3)

#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
using namespace std;

const int maxn=5e3+5;
int n;
int a[maxn];
int dp[maxn][maxn];

int main(){
    cin>>n;
    for(int i=0;i<n;i++)cin>>a[i];
    vector<int>v;
    for(int i=0;i<n;){
        int c=a[i];
        while(a[i]==c)i++;
        v.push_back(c);
    }
    int len=v.size();
    int mx=0;
    for(int i=0;i<len;i++){//以每个点为分界点
        int x=i-1,y=i+1;
        for(int j=y;j<len;j++)dp[0][j]=0;
        for(int j=x;j>=0;j--)dp[j][0]=0;//注意初始化
        for(int l=x;l>=0;l--){//注意l是从逆向遍历的
            for(int r=y;r<len;r++){
                if(v[l]==v[r])dp[l][r]=max(dp[l+1][r-1]+1,max(dp[l+1][r],dp[l][r-1]));
                else dp[l][r]=max(dp[l+1][r],dp[l][r-1]);
                mx=max(dp[l][r],mx);
            }
        }
    }
    cout<<len-1-mx<<endl;
    // cout<<mx<<endl;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值