CUSTACM Summer Camp 2022 Training 3(10题)

CUSTACM Summer Camp 2022 Training 3

A. Fillomino 2

题意

n*n的网格上,把1到n的数放在对角线上,要求把对角线以下(包括对角线)分成n个区域

  1. 每个区域都是相连的
  2. 第x个区域必须包含对角线上填有x的网格
  3. 第x个区域有包含x格网格,并且网格中数字都为x

给你n的大小以及对角线上数字的分布,问是否可以划分为符合以上条件的n个区域,如果能输出划分结果否则输出-1

数据范围: 1 ≤ n ≤ 500 1 \leq n \leq 500 1n500

tags:思维

思路

结果是一定可以划分

因为我们可以从最左上角开始划分,而且可以保证该划分不会影响后面的划分

  1. 如果能填左边就填左边
  2. 否则填下面
  3. 再否则填右边

因为按照这样划分是对整个区域(对角线以下的区域)外层填充,每次都从外圈一点点把里面区域填满,前面的划分自然不会影响到后面的划分

代码

#include<iostream>
using namespace std;

const int maxn=505;
int n;
int a[maxn][maxn];

int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        int x;cin>>x;
        a[i][i]=x;
    }
    for(int i=1;i<=n;i++){
        int x=i,y=i,t=a[i][i];
        for(int j=0;j<t-1;j++){//填t-1次数字a[i][i]
            if(a[x][y-1]==0&&y-1>=1){//能填左边就填左边
                a[x][--y]=t;
                continue;
            }
            else if(a[x+1][y]==0&&x+1<=n){//不能填左边就填下面
                a[++x][y]=t;
                continue;
            }
            else if(a[x][y+1]==0){//不能填左边和下面就往右边填
                a[x][++y]=t;
                continue;
            }
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=i;j++){
            cout<<a[i][j]<<' ';
        }
        cout<<endl;
    }
}

复杂度

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

B. AGAGA XOOORRR

题意

给你n个数字,可以把任意相邻的两个数字a、b换成a^b,问能否是这把其变为一串数字都相等的序列(该序列至少要有两个数)

数据大小: 1 ≤ t ≤ 15 , 2 ≤ n ≤ 2000 , 0 ≤ a i ≤ 2 30 1 \leq t \leq 15,2 \leq n \leq 2000,0 \leq a_i \leq 2^{30} 1t15,2n2000,0ai230

tags:位运算

思路

如果能变成数字都相等(为x)的序列,这些数字在继续执行两两异或操作,只会有两种可能

  1. 序列长度为偶数,最终结果为0
  2. 序列长度为奇数,最终为x

因为异或运算与顺序无关!!,我们就可以从把开始的n个数字做异或和

  1. 若结果为0,那么一定可以达到上面目的(只有达到”序列长度为偶数且数字都相等“这个中间状态,才会使异或和为0)
  2. 若结果为一个数y,看这些异或和是否可以组成 ≥ 2 \geq2 2个y即可(要最后更具y的值判断是否可以达到”序列长度为奇数且异或和为y“这个状态)

代码

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

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int t;
    for(cin>>t;t;t--){
        int n;
        cin>>n;
        int a[2005];
        ll x=0;
        for(int i=0;i<n;i++){
            cin>>a[i];
            x^=a[i];//异或和
        }
        if(!x)cout<<"YES"<<endl;//若异或和为0,则满足条件
        else{//若异或和非零,若想满足条件,则需让n个数异或成大于等于2个x(异或和)
            ll count=0,y=0;
            for(int i=0;i<n;i++){
                y^=a[i];
                if(y==x){//异或到=x,就重新开始奇数
                    count++;
                    y=0;
                }
            }
            if(y==0&&count>=2)cout<<"YES"<<endl;//要最后判断以下最后一次异或是否可以得到x,也就是y会不会变为0
            else cout<<"NO"<<endl;
        }

    }
}

复杂度

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

思维

很多关于异或的题目突破口都是异或与顺序无关来就异或和,用异或和作为突破口


C. Phoenix and Socks

题意

有n个袜子,有l个左袜子和r个右袜子(l+r=n),每个袜子都有颜色,有三种操作

  1. 改变其颜色为任意一种
  2. 把左袜子变为右袜子,不改颜色
  3. 把右袜子变为左袜子,不改颜色

问得到n/2双左右和颜色匹配的袜子的最小操作数

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

tags:贪心

思路

  1. 能匹配好的袜子先匹配,并剩下lx个左袜子,rx个右袜子
  2. 如果lx=rx,就只能一个个改颜色来匹配
  3. 如果lx!=rx,从袜子多(加上左袜子多)的哪一堆自己匹配(把颜色相同的数量大于2的左袜子中其中一个变为右袜子),直到不能匹配或者达到lx=rx为止
    1. 若是不能匹配为止,就得翻转袜子使lx=rx了,然后在一个个改颜色
    2. 若达到lx=rx为止,就一个个改颜色

代码

#include<iostream>
#include<cmath>
using namespace std;

const int maxn=2e5+5;
int n,l,r;
int c[maxn],ll[maxn],rr[maxn];

void dif(){
    for(int i=1;i<=n;i++){
        while(ll[c[i]]>0&&rr[c[i]]>0)ll[c[i]]--,rr[c[i]]--,l--,r--;
    }
}

int self(int*x){
    int count=0;
    for(int i=1;i<=n;i++){
        while(x[c[i]]>=2){
            x[c[i]]-=2;
            count++;
        }
    }
    return count;
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int t;
    for(cin>>t;t;t--){
        cin>>n>>l>>r;
        for(int i=1;i<=n;i++)ll[i]=rr[i]=0;//多组输入注意初始化
        for(int i=1;i<=n;i++){
            cin>>c[i];
            if(i<=l)ll[c[i]]++;//记录左袜子颜色为c[i]的数量
            else rr[c[i]]++;//记录右袜子颜色为c[i]的数量
        }
        dif();//先把可以匹配的匹配完
        int lc=self(ll),rc=self(rr);//在计算左袜子和右袜子各自可以形成多少次自匹配
        if(l>r){
            int x=(l-r)/2;//计算需要多少次自匹配
            if(lc>=x){
                cout<<x+r<<endl;//如果可执行的自匹配数>需要的自匹配数,就执行x+l次(自匹配需要翻转的次数+一次次改颜色的次数)
            }
            else{
                cout<<x+(x-lc)+r<<endl;//否则就要多执行(x-lc)次改颜色
            }
        }
        else if(l<r){
            int x=(r-l)/2;
            if(rc>=x){
                cout<<x+l<<endl;
            }
            else {
                cout<<x+(x-rc)+l<<endl;
            }
        }
        else cout<<l<<endl;//如果l=r,就只用执行一个个改颜色l次
    }
}

复杂度

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


D. Berland Regional

题意

有n个人,第i个人的属于ui,成绩为ai

举办比赛,每轮比赛要每个学校出成绩高的前k个人组成一队,举行到没有学校能出一队为止

参加比赛的所有人的总成绩为x。

输出k分别为1到n时的x

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

tags:前缀和

思路

利用vector把学生划分到各个学校中,然后将其成绩排序,在求好前缀和

对于k,每个学校(由x人)的贡献为sum[x/k*k-1]也就是前x/k *k个人的成绩总和嘛

注意:不要外层循环去枚举k里层循环去枚举n,这样的复杂度为n2了,而且因为k很大的时候会由很多一个学校出不了队伍而导致n的循环多余

代码

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

const int maxn=2e5+5;
int n;
int a[maxn],b[maxn];
vector<ll>u[maxn];
ll ans[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=1;i<=n;i++){
            u[i].clear();
            ans[i]=0;
        }
        for(int i=0;i<n;i++)cin>>a[i];
        for(int i=0;i<n;i++)cin>>b[i];
        for(int i=0;i<n;i++)u[a[i]].push_back(b[i]);//学生归类
        for(int i=1;i<=n;i++){
            if(!u[i].size())continue;
            sort(u[i].begin(),u[i].end(),greater<ll>());//从小到大排序
            for(int j=1;j<u[i].size();j++)u[i][j]+=u[i][j-1];//求前缀和
        }
        for(int i=1;i<=n;i++){
            int len=u[i].size();
            for(int k=1;k<=len;k++){//只有枚举k到学校人数,当k大于学校人数时该学校出不了人,a[k]的贡献为0
                ans[k]+=u[i][len/k*k-1];
            }
        }
        for(int i=1;i<=n;i++)cout<<ans[i]<<' ';
        cout<<endl;
    }
}

复杂度

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


E. Restoring the Permutation

题意

给一数组,求原数组,给定的数组的第i位表示原数组1到i的最大值。
输出可能的原数组的最小字典序和最大字典序。

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

tags:二分,思维

思路

  1. 第一次出现的数一定是固定的,我们只需去填那些非固定的数就行了
  2. 对于最小字典序,把未固定数排序,最小的数依次放到空位置就行,不会影响该数组结构(因为每次都是放最小的数)
  3. 但是对于最大字典序,如果把大的数放在前面,可能会改变数组应有的结构,所以不能满目的把最大的数依次放空位置,而是放比该空位置前固定的数x小的数中(未固定的数中)的最大数(要用二分查找)

代码

#include<iostream>
#include<algorithm>
#include<map>
#include<vector>
using namespace std;

const int maxn=2e5+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;
        int b[maxn]={0};
        map<int,int>appear;
        vector<int>no;
        for(int i=1;i<=n;i++){
            cin>>a[i];
            if(i!=1&&a[i]!=a[i-1])b[i]=a[i],appear[a[i]]++;//appear是已固定的数
        }
        b[1]=a[1],appear[a[1]]++;
        for(int i=1;i<=n;i++)if(!appear[i])no.push_back(i);//no中是未固定的数
        sort(no.begin(),no.end());//从小到大排序
        int x=0;
        for(int i=1;i<=n;i++){//对于最小字典序,固定的输出,空位置输出no中的最小数
            if(b[i])cout<<b[i]<<' ';
            else cout<<no[x++]<<' ';
        }
        cout<<endl;
        // sort(no.begin(),no.end(),greater<int>());
        x=0;
        for(int i=1;i<=n;i++){
            if(b[i]){
                x=b[i];//固定的直接输出,并保存
                cout<<b[i]<<' ';
            }
            else{
                // for(int j=0;j<no.size();j++){
                //     if(no[j]<x){
                //         cout<<no[j]<<' ';
                //         no.erase(no.begin()+j);
                //         break;
                //     }
                // }
                int it=upper_bound(no.begin(),no.end(),x)-no.begin()-1;//用二分!!,no中第一个大于x(空位置前第一个固定的数)后的数一定是<x的数中最大的数
                cout<<no[it]<<' ';
                no.erase(no.begin()+it);//输出并删除
            }
        }
        cout<<endl;
    }
}

复杂度

O ( n l o g n ) O(nlogn) O(nlogn)


F. Planar Reflections动规好题

题意

假设现在有n个平面,从左到右排列,现有一个衰变年龄为k kk的粒子从最左端向右发射

如果一个粒子碰到了一个平面,它将会不改变衰变年龄保持运动方向并传过这个平面

如果这个粒子的衰变年龄大于1,那么将会分裂出一个衰变年龄为k-1的往相反方向运动的粒子

给定n和k,问这个空间最终会有多少个粒子,答案对1e9+7取模

数据范围:1≤T≤100,1≤n,k≤1000,∑n≤1000,∑k≤1000

tags:动态规划

思路

很有意思的动态规划

  1. 抽象意义dp[i][j]表示前方还有i个屏幕衰变年龄为j粒子对空间中粒子数的贡献
  2. 边界处理
    1. 对于衰败年龄为1的粒子,不管前面有多少个屏幕,它的贡献都为1, dp[0~n][1] = 1;
    2. 对于前方屏幕为0的情况,不管粒子衰变年龄为多少,它的贡献都为1, dp[0][1~k] = 1;
  3. 状态转移dp[i][j] = (dp[i-1][j] + dp[n-i][j-1]) % mod;前方有i-1个屏幕衰变年龄为j的贡献+前方有n-i个屏幕衰变数量为j-1的贡献
  4. 遍历顺序:注意不能直接照搬状态转移方程,因为可以发现在计算dp[i][j]时其来源dp[n-1][j-1]可能还未确定。所以得先对衰变年龄遍历在对屏幕数遍历
  5. 最终结果dp[n][k]

代码

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

const int maxn = 1e3 + 5, mod = 1e9 + 7;
int n, k;
ll dp[maxn][maxn];

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int t;
    for (cin >> t; t; t--)
    {
        cin >> n >> k;
        for (int i = 0; i <= n; i++)
            dp[i][1] = 1;
        for (int i = 1; i <= k; i++)
            dp[0][i] = 1;
        for (int i = 2; i <= k; i++)
        {
            for (int j = 1; j <= n; j++)
            {
                dp[j][i] = (dp[j - 1][i] + dp[n - j][i - 1]) % mod;
            }
        }
        cout << dp[n][k] << endl;
    }
}

复杂度

O ( k ∗ n ) O(k*n) O(kn)

思维

对于动态规划,不要总是去想它的中间过程的细节,严格按照上面的步骤来分析就行:抽象意义→边界处理→状态转移方程→遍历顺序→最终结果


G. Building a Fence

题意

给了n个宽为1,高为k的栅栏,以及凹凸不平的地,长度为h1,先要求将栅栏放到这地上,要求

  • 第一个栅栏和最后一个栅栏只能放到地上
  • 中间的栅栏最多可以高于地面k−1的高度放置
  • 相邻栅栏至少有高度1的单位是毗邻的

问是否存在放置要求满足以上方案。

数据范围: 2 ≤ n ≤ 2 ∗ 1 0 5 , 2 ≤ k ≤ 1 0 8 , 0 ≤ h i ≤ 1 0 8 2 \leq n \leq 2*10^5,2 \leq k \leq 10^8,0 \leq h_i \leq 10^8 2n2105,2k108,0hi108

tags:思维,递推

思路

假设每一个栅栏,其前一个栅栏底部的最底高度为down,最高高度为up。那么对于下一个栅栏的底部:

  1. 最底高度:
    1. 肯定会大于地面高度 h i h_i hi
    2. 与前一个栅栏底部(底部为的最低高度)除只有1米毗邻往下 d o w n 前 − k + 1 down_前-k+1 downk+1
  2. 最高高度:
    1. 高于地面 k − 1 k-1 k1
    2. 与前一个栅栏顶部(底部为最高高度)除只有1米毗邻往上 u p 前 + k − 1 up_前+k-1 up+k1
  3. max(down-k+1,h[i])<=min(up+k-1,h[i]+k-1)是该栅栏就可放了,并更新down与up

最后要特判一下最后一个栅栏是否可以放在地上

代码

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

const int maxn=2e5+5;
int n,k;
int h[maxn];

bool judge(){
        int down=h[0],up=h[0];
        for(int i=1;i<n-1;i++){
            down=max(down-k+1,h[i]);
            up=min(up+k-1,h[i]+k-1);
            if(down>up)return false;
        }
        down=down-k+1,up=up+k-1;
        return (h[n-1]>=down&&h[n-1]<=up);//特判是否可以放在地上
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int t;
    for(cin>>t;t;t--){
        cin>>n>>k;
        for(int i=0;i<n;i++)cin>>h[i];
        if(judge())cout<<"YES"<<endl;
        else cout<<"NO"<<endl;
    }
}

复杂度

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


H. Ceil Divisions

题意

在这里插入图片描述

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

tags:思维

思路

因为**[x/x+1]=1**

所以我们可以从3开始一直执行n-3次此操作,最后剩下n

我们还有8次操作可以使用,但是如果n很大时对n进行8次除2取整并不能使其变为1

但是要知道指数的增长很快的,既然除2不行,那就除8,我们就会剩下6次对n除8取整的操作,可以计算86大于2*105

代码

#include<iostream>
using namespace std;

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

int main(){
    int t;
    for(cin>>t;t;t--){
        cin>>n;
        if(n<=32){//n<=32时,可以用2来除最后的n
            int count=0,x=n;
            while(x!=1){
                count++;
                x=((x+1)>>1);
            }
            cout<<count+n-3<<endl;
            for(int i=3;i<=n-1;i++)cout<<i<<' '<<i+1<<endl;
            while(count--)cout<<n<<' '<<2<<endl;
        }
        else{//否则用8来出最后的n
            int count=0,x=n;
            while(x!=1){
                count++;
                x=((x+7)/8);//向上取整
            }
            cout<<n-4+3+count<<endl;
            for(int i=3;i<=n-1;i++){
                if(i==8)continue;
                cout<<i<<' '<<i+1<<endl;
            }
            while(count--)cout<<n<<' '<<8<<endl;
            cout<<8<<' '<<2<<endl;
            cout<<8<<' '<<2<<endl;
            cout<<8<<' '<<2<<endl;
        }
    }
}

复杂度

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


I. Pekora and Trampoline差分

题意

有n个跳床,每个跳床有一个跳的距离,每个跳床跳一次就会使得弹跳距离减1,弹跳距离max(1,a[i])。你可以选择任意跳床选择开始,问你最少经过几个轮回才能将所有跳床弹跳距离都变成1。

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

tags:差分,贪心

思路

**贪心:**从第一个弹力不为1的蹦床开始跳,直到它=1,在继续下一个蹦床

**差分:**要直到从一个蹦床开始弹跳会对让后面的蹦床有怎样的白嫖机会

  1. 若si+i>n,则一直蹦直到si+i=n
  2. 若si+i<=n,要进行总共s[i]-1轮从该蹦床出发使其变为1,那么他会让第i+2到第si+i个蹦床都白嫖一次
  3. 如果先前基类的白嫖次数count[i]>s[i]-1,那么它就可以不用进行s[i]-1轮了,并且会让第i+1个蹦床有count[i]-s[i]+1次白嫖
  4. 若count[i]<=s[i]-1那么就要进行s[i]-1-count[i]轮补充来实现总轮数为s[i]-1

代码

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

const int maxn=5e3+4;
int n;
ll s[maxn],count[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=1;i<=n;i++){
            cin>>s[i];
            count[i]=0;
        }
        ll ans=0;
        for(int i=1;i<=n;i++){
            count[i]+=count[i-1];//count是差分数组,求前缀和来求积累的白嫖次数
            int res=0;
            if(s[i]+i>n){//若s[i]+i>n要进行s[i]+i-n轮使s[i]+i=n
                res+=s[i]+i-n;
                s[i]=n-i;
            }
            res+=s[i]-1;//在需进行s[i]-1轮使该蹦床弹力为0
            if(count[i]>res)count[i+1]+=count[i]-res,count[i+2]-=count[i]-res;
            count[i+2]++,count[i+s[i]+1]--;//利用差分数组来更新对后面蹦床的白嫖次数
            ans+=max(0ll,res-count[i]);//看先前积累的白嫖数count[i]是否达到所需轮数,不够就得ans+=来补充了
        }
        cout<<ans<<endl;
    }
}

复杂度

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


J. RPD and Rap Sheet (Easy Version)交互好题

题意

在这里插入图片描述

tags:交互,思维,构造

思路

可以知道旧密码^询问值=新密码

我们从0到n-1遍历旧密码

对于询问值,我们需要猜,直到猜对返回1为止。假设遍历到第x个旧密码我们已经猜了的询问值为 a 1 , a 2 , a 3 . . . a x a_1,a_2,a_3...a_x a1,a2,a3...ax

不要关中间变化的新密码,只用管最后的新密码,并且假设旧密码值一直不会变

我们这次的新密码就是x^a[1]^a[2]^...^a[x],我们此次就猜这个数,如果不对说明我们就旧密码是错的,就换一个旧密码,并将此次猜的数字加入到询问值的异或和中

因为旧密码是0到n-1里的一个数,所以一定可以猜到新密码的

代码

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

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int t;
    for(cin>>t;t;t--){
        int n,k;
        cin>>n>>k;
        ll res=0,sum=0;
        for(ll i=0;i<n;i++){//假设旧密码为i
            ll x=(i^sum);//那么新密码就为i^sum,我们就猜i^sum
            cout<<x<<endl;
            int judge;
            cin>>judge;//若judge为1,说明新密码就是我们猜的,也说明旧密码是i
            if(judge)break;
            sum^=x;//若judge为0,说明旧密码不是i,继续遍历旧密码,但是这次猜测我们还是得加入到询问异或和中
        }
    }
}

复杂度

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值