天梯赛校内选拔题解

L1-4

题意:praying找到了一群史莱姆,它们在一排格子上阴暗地爬行。
一共有n个格子,初始时每个格子上都有一只史莱姆。史莱姆们各自有一个移动方向,向左或者向右,位于格子x的史莱姆向左会走到x−1,向右会走到x+1,格子的范围是[1,n],超过范围的史莱姆会立刻融化。
史莱姆们每秒移动一次,问第1到第n秒每一秒结束时,有多少个格子没有史莱姆。多只史莱姆可以同时在一个格子上。
输入格式
第一行一个正整数n(n≤300)
第二行n个整数,整数只会是0或1,0表示该位置上史莱姆会往左走,1表示往右走。
输出格式
一行n个整数表示每秒钟结束后有多少个格子没有史莱姆。

考场做法:我最先得出的结论是有多少史莱姆移出格子外就有多少个格子没有史莱姆,但因为一个格子可以有多个史莱姆所以错得离谱。后面想着去模拟每只史莱姆的运动过程统计有多少个格子没有史莱姆,但发现这种维护几乎是不可能的(麻烦+复杂度高),但后面仔细想想会发现,每一个史莱姆的初始位置和速度定了,那么它们在任意时刻的位置都是确定的,这个时候统计即可,破案了,高中物理题(

L1-7

题意:给出一个包含n个数字数组,每次操作可以选择将一个数字+1或者−1。
对于每个i(i=1…n),求在保持a 不变的情况下,最少需要多少次操作能够让数组所有数字和a 相同
输入格式
第一行一个正整数n(n≤1e5)
第二行n个整数a(1,1e9)
输出格式
n行每行一个整数表示答案

考场做法:考试时只写了暴力方法,其实考试的时候已经意识到了与前缀后有关而且需要将原数组排序,但是当时只剩几分钟了看榜单以为另一道题好写就没写这道了(
正解:对排序后的数组求前缀和数组得到s数组,然后枚举每一个i,推出公式s[n]-2s[i]+(2i-n)*a[i].var即可求出每个位置对应的答案。
代码

#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N = 1e5+7;
struct node {
    int biao;
    ll var;
}a[N];
ll res[N];
ll s[N];

bool cmp(node a,node b) {
    return a.var < b.var;
}

int main() {
    int n;
    cin >> n;
    for(int i = 1;i <= n;i++) scanf("%d",&a[i].var),a[i].biao=i;

    sort(a+1,a+1+n,cmp);
    for(int i = 1;i<=n;i++) s[i] = s[i-1] + a[i].var;
    for(int i = 1;i <= n;i++) {
        res[a[i].biao] = s[n]-2*s[i]+(2*i-n)*a[i].var;
    }
    for(int i =1;i<=n;i++) cout<<res[i]<<endl;

    return 0;
}


L1-8

题意:博弈计算中,经常需要使用乘法,现在有一个关于乘法的问题:给定数字n,请问有多少种不同的方案将n分解为若干个大于1的整数的乘积?
为了方便起见,这里的乘积方案只考虑组合,不考虑排列。
举个例子:当n=6,方案6=2∗3和6=3∗2视为同一种方案。
输入格式
一行一个正整数n(n≤35000)
输出格式
一行一个正整数,表示分解的方案数。

考场做法:考试的时候感觉像是dp题但又不知道怎么dp,最后放弃了。
补题做法:思考解空间发现,我们可以按照方案中最小的因数来枚举方案,即枚举方案中出现的最小因数。考虑用dfs来做,对于每个枚举到的因数i,可以继续搜索dfs(i,n/i)有多少种方案,dfs(i,n/i)意思为将n/i这个数分解为最小因数大于等于i的方案数,后面用递归做即可。
代码:(可能有些冗余)

#include<iostream>

using namespace std;
long long ans = 1;

void dfs(int x, int n) {
    for(int i = x;i*i<=n;i++) {
        if(n%i==0&&n/i>=i) {
            ans++;
            dfs(i,n/i);
        }
    }
}

int main() {
    int n;
    cin >> n;
    for(int i = 2;i * i <= n;i++) {
        if(n%i==0&&n/i>=i) {
            ans++;
            dfs(i,n/i);
        }
    }

    cout << ans << endl;

    return 0;
}

L2-1

题意:给你一个只包含”(“和”)”的字符串,求有多少个子串是合法的括号序列。
输入格式
第一行一个正整数n(n≤1000)表示序列长度
第二行一个字符串,如题
输出格式
一行一个整数表示答案

考场做法:看到括号序列就脑阔疼,考试的时候一直在想(())和()()这两种情况,写出来后发现处理不了(()())这种序列,最后时间也没写出来。
题解:枚举每一个右括号,判断这个右括号能构成多少个左括号。考试时也是这么想的,但是没思考出来如何统计这个右括号能构成多少合法序列,看了题解后发现,合法序列具有“延续性”,当你往左枚举时,如果右括号多了那么可以继续往左枚举,但如果左括号多了那么再往左就不可能再构成合法序列了,此时退出循环。
代码

#include<iostream>

using namespace std;
const int N = 1e3+5;
int a[N];
//int s[N];

int main() {
    int n;
    cin >> n;
    char c;
    for(int i = 1;i <= n;i++) {
        cin >> c;
        if(c=='(') a[i] = 1;
        else a[i] = -1;
        //s[i] = s[i-1] + a[i];
    }
    int ans = 0;
    for(int i = 1;i <= n;i++) {
        if(a[i] == -1) {
            int s = -1;
            int j = i - 1;
            while(j>=1) {
                s += a[j];
                if(s==0) ans++;
                else if(s>0) break;
                j--;
            }
        }
    }
    cout << ans << endl;
    return 0;
}

L2-2

题意:图有n个点,点从1开始编号到n,每个点都有一条单向出边。
求图中有几个环
输入格式
第一行一个正整数n(n≤1000)
第二行n个正整数,第i个正整数表示点i的出边连向的点的编号,保证数据合法
输出格式
一行一个整数表示答案

考场做法:对于每个点,如果没被访问过就开始dfs找环,找到返回1,没找到返回0,找到的话将路径上所有点都标记为访问过,结果wa了。
题解:其实错的很离谱。每一个点都有一条出边,也就是说从每一个点出发是一定能够遍历到环的,因为最后的点一定会指向路径上的点否则路径会一直延续,也就是说dfs函数一定会返回1,那为什么wa了呢?因为可能会有很多点的路径指向同一个环,那么这个环就会被重复计算多次,所以dfs函数中应该还要加一个如果遇到被访问的点(环已被访问过)就返回0的条件。
代码

#include<iostream>

using namespace std;
const int N = 1e3+7;

int a[N];
int vis[N];

bool dfs(int n) {
    if(vis[n]==-1) return 1;
    if(vis[n]==1) return 0;
    vis[n] = -1;
    int x = dfs(a[n]);
    vis[n] = 1;
    return x;
}

int main() {
    int n;
    cin >> n;
    int ans = 0;
    for(int i = 1;i <= n;i++) cin >> a[i];
    for(int i = 1;i <= n;i++) {
        if(vis[i]!=1&&dfs(i)) ans++;
    }
    cout << ans << endl;
    return 0;
}

L2-4

题意:有一棵有n个节点的有根树,树根是m,节点编号从1到n,两个节点x和y可以产生共鸣,当且仅当:
1.y在以x为根的子树内。
2.x是y的倍数。
将树和根节点编号给出,请回答有多少对节点能产生共鸣。
输入格式
第一行两个正整数n,m(n≤5∗1e5,1≤m≤n)
接下来n−1行每行两个正整数,描述一条树边,保证输入数据合法。
输出格式
一行一个整数,表示答案。

考场做法:想什么,考场看到这题直接跳了。
题解:用dfs序判断子树关系,然后枚举因子,再枚举倍数,判断两个点是否构成子树关系。dfs序:得到每个节点进队的时间戳和出队的时间戳,性质:一个节点进队出队之间遍历的节点为该节点子树。
代码

#include<iostream>
#include<vector>
using namespace std;
const int N = 5e5+7;
typedef long long ll;
ll in[N];
ll out[N];
int vis[N];
//int s[N];
ll cnt = 0;

vector<int> e[N];
void dfs(int n) {
    vis[n] = 1;
    in[n] = ++cnt;
    for(int u:e[n]) {
        if(!vis[u]) dfs(u);
    }
    out[n]=++cnt;
    //return s[n];
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    int n,m;
    cin >> n >> m;
    int u,v;
    for(int i = 1;i < n;i++) {
        cin >> u >> v;
        e[u].push_back(v);
        e[v].push_back(u);
    }
    //for(int i =1;i<=n;i++) s[i]=1;
    dfs(m);
    int ans = n;
    //for(int i =1;i<=cnt;i++) cout<<a[i]<<" " <<s[a[i]]<<endl;
    for(int i = 1;i<=(n+1)>>1;i++) {
        for(int j = 2;1ll*i*j<=n;j++) {
            if((in[i]>=in[i*j]&&out[i]<=out[j*i]))
                //cout<<i<<" "<<j*i<<endl,
                ans++;
        }
    }
    cout<<ans<<endl;

    return 0;
}

L3-1

题意:消除游戏是这样子的,有一排砖块,共n块,每块砖都有一个颜色,每次可以选择消除一块砖、消除两块连续的相同颜色的砖或消除三块连续的相同颜色的砖,分别可以获得a,b,c分。消除掉的砖块会立即消失,其两边的砖块会挨在一起。
praying游戏技术很糟糕,请你告诉他最优策略下可以获得最多多少分。
输入格式
第一行四个整数n,a,b,c(1≤n≤300,0≤a,b,c≤40000)
第二行n个正整数从左到右描述每块砖的颜色,颜色编号不超过n
输出格式
一行一个整数表示答案

考场做法:看到就跳了(
题解:dp题,设计状态dp[i][j]表示消除区间i到j的最大值,思考状态转移方程由什么构成,首先如果单消的话就是max(dp[i+1][j],dp[i][j-1])+a,双消三消的话都要求f[i]==f[j],双消为dp[i+1][j-1]+b,三消的话枚举i,j区间中满足f[k]==f[i]的k值,分数为dp[i+1][k-1]+dp[k+1][j-1]+c,然后这样写出来的话会发现还是答案错误,在praying大佬的帮助下发现需要加一句for (int k=i;k<j;k++) dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]); 为什么呢?这句代码个人感觉非常突兀,你把它放在那会感觉很正常就该是这样,但是不把它放那我无论如何都想不到加这句话,这句话在我的理解就是为dp[i][j]赋上了一个初值,这个初值由两段区间加和构成,容易想到这两段区间的值都是之前求过的值所以不为0,那为什么要这么做呢?当i与j颜色不同时,是不会考虑双消三消的,但是你能保证i,j区间要拿到最大分数的话里面不会发生双消三消吗?答案显而易见,就是状态转移方程漏考虑了,那怎么补上缺失的状态呢?其实就是那句代码,当枚举到i,j区间时,其实这个区间已经有一个能获得的最大分数了,但不管怎么想,还是觉得以后再看这题也想不出来www。
代码

#include<iostream>

using namespace std;
const int N = 310;
typedef long long ll;
ll f[N];
ll dp[N][N];

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int n,a,b,c;
    cin >> n >> a >> b >> c;
    for(int i =1;i <= n;i++) cin >> f[i];

    for(int i = n;i >= 1;i--) {
        for(int j = i;j<=n;j++) {
            dp[i][j] = max(dp[i+1][j],dp[i][j-1])+a;
            for (int k=i;k<j;k++) dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]);
            if(i!=j&&f[i]==f[j]) {       
                dp[i][j] = max(dp[i][j],dp[i+1][j-1]+b);
                for(int k = i+1;k<=j-1;k++) {
                    if(f[i]==f[k])
                        dp[i][j] = max(dp[i][j],dp[i+1][k-1]+dp[k+1][j-1]+c);
                }
            }
        
        }
    }
    cout<<dp[1][n] << endl;
    return 0;
}

L3-2

题意:想必大家都学过快速排序算法,简单来讲就是利用分治法进行排序,每次选择区间中的一个数x作为标准,将区间内比x小的数字放在左边,比x大的数字放在右边,然后再递归分治左边的区间和右边的区间。直到区间大小为1时停止分治。
praying脑子不太正常,在赛场上手忙脚乱的他写出了惊为天人的丑陋快排,他的代码有一个致命bug,那就是在区间大小不为1的时候有概率不选择数字x并直接退出分治。
这样一来,排序的结果可能就完全不正确了。
现在悲惨的是,praying的代码甚至不能输出,他只能知道每次分治选择的x是多少。
为了统一标准,praying的快速排序过程中,将数字分居x的左右,左边和右边不会改变原本数字的相对顺序。举个例子就是,对[2,3,5,4,1]排序时,选取x=3作为标准,1和2需要放在左边,4和5需要放在右边,因此本区间的划分结果就是[2,1,3,5,4],1和2的相对顺序不改变,4和5的相对顺序不改变。
现在praying给出原本的序列,以及每次分治选取的x,你需要告诉他排序的结果。
注意:x的给出顺序不一定是分治的顺序,实际上这题与x的排列方式无关,在x是哪些数字已经确定的情况下,答案就是唯一的。
输入格式
第一行一个正整数n(n≤1e5),表示原排序数组长度
第二行n个不超过n且互不相同的正整数
第三行一个整数m(0≤m≤n),表示选取x的个数
第四行m个正整数,保证数据合法
输出格式
一行,n个正整数,表示排序后的结果

考场做法:压根看不到这道题
题解:题目已经告诉最终结果与x的给出顺序无关,容易发现我们可以将给出的x从小到大排个序之后,每个x作用过后左边的数便已经确定了,剩余的x只会影响右边区间,所以我们考虑将每个数放到该放的两x区间内,由于x值有序,所以对于每个数可以用二分判定它的左边的x和右边的x,为了方便存储与输出,我们可以将每个数挂靠在右边的x上,开m+1个vector,然后每个vector内存储挂靠在这个x上的数,第一个x之前的数挂靠在x=0上。
代码

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int N = 1e5+7;
int a[N];
int b[N];
int vis[N];
vector<int> e[N];

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int n,m;
    cin >> n;
    for(int i = 1;i <= n;i++) cin >> a[i];
    cin>>m;
    for(int i =1;i <= m;i++) cin >> b[i];

    vector<int> ans;
    sort(b+1,b+m+1);

    for(int i = 1;i <= n;i++) {
        int l = 0,r = m;
        while(l<=r) {
            int mid = (l+r)>>1;
            if(b[mid]>a[i]) r = mid -1;
            else l = mid + 1;
        }
        e[b[r]].push_back(a[i]);
    }

    for(int i = 0;i<=m;i++) {
        if(i!=0) cout<<b[i]<<" ";
        for(int j = 0;j < e[b[i]].size();j++) {
            if(e[b[i]][j]!=b[i]) cout<<e[b[i]][j]<<" ";
        }
    }
    cout<<endl;
    return 0;

}

L3-3

题意:praying获得了一个矩阵生成器,这个矩阵生成器可以这样生成n行m列的矩阵:
给生成器两个序列a[n],b[m],矩阵的第i行第j列就是a[i] xor b[j]
其中xor是二进制按位异或操作,具体运算律如下:
0 xor 0=0
1 xor 1=0
0 xor 1=1
1 xor 0=1
现在praying知道a[n],b[m],希望你求出生成矩阵的子矩阵异或和最大值是多少。
子矩阵异或和:子矩阵中的元素异或起来得到的值,由运算律得知,若干个数字的异或和与计算顺序无关,只与这些数是什么有关。
输入格式
第一行两个正整数n,m(n,m≤1000)
第二行为n个数字描述a[n]
第三行为m个数字描述b[m]
0≤a[i],b[i]≤2e29
输出格式
一行一个整数表示答案

题解:待补

  • 43
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秭归云深处

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值