CSP-J2020复赛题解

CSP-J复赛教程:CSP/NOIP/C++信息学奥赛-复赛篇教程,参加复赛的同学们不要错过。

T1:优秀的拆分(power)

题目描述

一般来说,一个正整数可以拆分成若干个正整数的和。例如, 1 = 1, 10 = 1 + 2 + 3 + 4 等。
对于正整数 n 的一种特定拆分,我们称它为“优秀的”,当且仅当在这种拆分下, n 被分解为了若干个不同的 2 的正整数次幂。注意, 一个数 x 能被表示成示成 2 的正整数次幂,当且仅当 x 能通过正整数个 2 相乘在一起得到。
例如, 10 = 8 + 2 = 23 + 21 是一个优秀的拆分。但是, 7 = 4 + 2 + 1 = 22 + 21 + 20 就不是一个优秀的拆分,因为 1 不是 2 的正整数次幂。
现在,给定正整数 n,你需要判断这个数的所有拆分中,是否存在优秀的拆分。 若存在, 请你给出具体的拆分方案。

输入

输入文件只有一行,一个正整数 n,代表需要判断的数。

输出

如果这个数的所有拆分中,存在优秀的拆分。那么, 你需要从大到小输出这个拆分中的每一个数, 相邻两个数之间用一个空格隔开。 可以证明, 在规定了拆分数字的顺序后, 该拆分方案是唯一的。
若不存在优秀的拆分,输出“-1”(不包含双引号)。

参考解法

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

/*
定义:分解为了若干个不同的 2 的正整数次幂
性质:是偶数,奇数不存在优秀的拆分

思路:
如果n是偶数,将n进行进制转换
10 -> 1010
*/
int a[110],n,k = 0;//k表示a数组的下标

int main(){
    cin>>n;
    //特判奇数的情况
    if(n % 2 != 0){
        cout<<-1;
        return 0;
    }

    //进制转换
    int t = 1;//表示2的次方
    while(n != 0){
        if(n % 2 != 0){
            k++;
            a[k] = t;
        }
        t = t * 2;
        n = n / 2;
    }

    //逆序输出结果
    for(int i = k;i >= 1;i--){
        cout<<a[i]<<" ";
    }
    return 0;
}

T2:直播获奖( live)

题目描述

NOI2130 即将举行。为了增加观赏性, CCF 决定逐一评出每个选手的成绩,并直播即时的获奖分数线。 本次竞赛的获奖率为 w%, 即当前排名前 w%的选手的最低成绩就是即时的分数线。
更具体地, 若当前已评出了 p 个选手的成绩, 则当前计划获奖人数为max(1, ⌊p × w%⌋), 其中 w 是获奖百分比, ⌊x⌋ 表示对 x 向下取整,
max(x, y) 表示 x 和 y 中较大的数。如有选手成绩相同,则所有成绩并列的选手都能获奖,因此实际获奖人数可能比计划中多。
作为评测组的技术人员,请你帮 CCF 写一个直播程序。

输入

第 1 行两个正整数 n, w。 分别代表选手总数与获奖率。
第 2 行有 n 个非负整数,依次代表逐一评出的选手成绩。

输出

只有一行,包含 n 个非负整数,依次代表选手成绩逐一评出后,即时的获奖分数线。 相邻两个整数间用一个空格分隔。

参考解法

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

/*
1.获奖人数为k = max(1, int(p × w%))
2.选手的成绩均为不超过 600 的非负整数
思路:使用数组统计每个分值得分的人数
从大到小统计出k个人,最后一次循环对应的分值就是要求的获奖分数
*/
int a[610];
int n,w,x;

int main(){
    scanf("%d%d",&n,&w);
    int k;//获奖人数
    int ans;//统计人数
    //读入n个人的分数,读一个算一个
    for(int i = 1;i <= n;i++){
        scanf("%d",&x);
        a[x]++;//统计每个分值对应的人数
        //计算获奖人数
        k = max(1,int(i * w / 100.0));
        ans = 0;
        //从最高分的人数向下统计出>=k个人
        for(int j = 600;j >= 0;j--){
            if(ans + a[j] < k) ans += a[j];//+=
            else{
                printf("%d ",j);
                break;
            }
        }
    }
	return 0;
}

T3:表达式(expr)

题目描述

小 C 热衷于学习数理逻辑。有一天,他发现了一种特别的逻辑表达式。在这种逻辑表达式中, 所有操作数都是变量,且它们的取值只能为 0 或 1, 运算从左往右进行。如果表达式中有括号,则先计算括号内的子表达式的值。特别的,这种表达式有且仅有以下几种运算:

1. 与运算: a & b。当且仅当 a 和 b 的值都为 1 时,该表达式的值为1。其余情况该表达式的值为 0。

2. 或运算: a | b。当且仅当 a 和 b 的值都为 0 时,该表达式的值为0。其余情况该表达式的值为 1。

3. 取反运算: !a。当且仅当 a 的值为 0 时,该表达式的值为 1。其余情况该表达式的值为 0。

小 C 想知道,给定一个逻辑表达式和其中每一个操作数的初始取值后, 再取反某一个操作数的值时,原表达式的值为多少。

为了化简对表达式的处理,我们有如下约定:

表达式将采用后缀表达式的方式输入。后缀表达式的定义如下:

1. 如果 E 是一个操作数,则 E 的后缀表达式是它本身。

2. 如果 E 是 E1 op E2 形式的表达式, 其中 op 是任何二元操作符, 且优先级不高于 E1、 E2 中括号外的操作符, 则 E 的后缀式为 E1' E2' op,其中 E1'、E2' 分别为 E1、 E2 的后缀式。

3. 如果 E 是 (E1) 形式的表达式,则 E1 的后缀式就是 E 的后缀式。
同时为了方便, 输入中:

a) 与运算符(&)、或运算符(|)、取反运算符(!)的左右均有一个空格,但表达式末尾没有空格
b) 操作数由小写字母 x 与一个正整数拼接而成,正整数表示这个变量的下标。例如: x10, 表示下标为 10 的变量 x10。数据保证每个变量在表达式中出现恰好一次

输入

第一行包含一个字符串 s,表示上文描述的表达式。
第二行包含一个正整数 n,表示表达式中变量的数量。表达式中变量的下标为 1,2, … , n。

第三行包含 n 个整数, 第 i 个整数表示变量 xi 的初值。

第四行包含一个正整数 q,表示询问的个数。

接下来 q 行,每行一个正整数,表示需要取反的变量的下标。注意,每一个询问的修改都是临时的,即之前询问中的修改不会对后续的询问造成影响。

数据保证输入的表达式合法。 变量的初值为 0 或 1。

输出

输出一共有 q 行,每行一个 0 或 1,表示该询问下表达式的值。

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

/*
取反某一个操作数的值时,原表达式的值为多少
思路:
1.读入表达式,将表达式建成二叉树
2.计算出表达式的值
3.深搜,求出哪些结点的值修改后对结果有影响
*/
const int N = 1e6 + 10;
struct node{
    int to,next;
}a[N];//邻接表
int pre[N],k;//存储以每个点为起点的最后一条边的编号
int num[N];//存储二叉树每个结点的值
char c[N];//存储操作符
int m;//表示c数组的下标
bool f[N];//标记哪些点的值修改后对结果有影响
int n;
string s,w;//w用来存储每个操作数的下标
stack<int> st;//用于计算后缀表达式

//建边
void add(int u,int v){
    k++;
    a[k].to = v;
    a[k].next = pre[u];
    pre[u] = k;
}

//深搜求哪些结点的值修改后对结果有影响
void dfs(int x){
    f[x] = true;
    if(x <= n) return;//如果是叶子结点
    if(c[x] == '!') dfs(a[pre[x]].to);
    else{
        int n1 = a[pre[x]].to,n2 = a[a[pre[x]].next].to;
        if(c[x] == '&'){
            if(num[n1] == 1) dfs(n2);
            if(num[n2] == 1) dfs(n1);
        }else if(c[x] == '|'){
            if(num[n1] == 0) dfs(n2);
            if(num[n2] == 0) dfs(n1);
        }
    }
}

int main()
{
    getline(cin,s);
    cin>>n;
    for(int i = 1;i <= n;i++){
        scanf("%d",&num[i]);
    }

    //分析表达式,建树
    int x,y;
    m = n;
    for(int i = 0;i < s.size();i++){
        if(isdigit(s[i])){
            w = w + s[i];
            //如果连续数字结束
            if(i==s.size()-1||!isdigit(s[i+1])){
                st.push(atoi(w.c_str()));//将操作数的下标入栈
                w = "";
            }
        }else if(s[i] == '!'){
            //如果是取反操作符
            m++;
            c[m] = s[i];//操作符的编号是m
            x = st.top();//取栈顶元素来运算
            st.pop(); //出栈
            //建边
            add(m,x);
            num[m] = !num[x];
            st.push(m);
        }else if(s[i] == '&' || s[i] == '|'){
            //如果是& |操作符
            m++;
            c[m] = s[i];
            //取2个操作数
            x = st.top();
            st.pop();
            y = st.top();
            st.pop();
            //建边
            add(m,x);
            add(m,y);
            //计算结点的值
            if(s[i] == '&') num[m] = num[x] & num[y];
            else if(s[i] == '|') num[m] = num[x] | num[y];

            st.push(m);
        }
    }

    int ans = num[st.top()];
    //cout<<ans;
    //深搜求哪些结点的值修改后对结果有影响
    dfs(st.top());//从根开始深搜

    //q次询问
    int q;
    cin>>q;
    while(q--){
        scanf("%d",&x);
        if(f[x]==true) printf("%d\n",!ans);
        else printf("%d\n",ans);
    }

    return 0;
}

T4:方格取数(number)

题目描述

设有 n × m 的方格图,每个方格中都有一个整数。现有一只小熊, 想从图的左上角走到右下角,每一步只能向上、向下或向右走一格, 并且不能重复经过已经走过的方格,也不能走出边界。小熊会取走所有经过的方格中的整数,求它能取到的整数之和的最大值。

输入

第 1 行两个正整数 n, m。
接下来 n 行每行 m 个整数, 依次代表每个方格中的整数。

输出

一个整数,表示小熊能取到的整数之和的最大值 。

参考程序

#include <bits/stdc++.h>
using namespace std;
/*
左上角走到右下角
每一步只能向上、向下或向右走一格
不能重复经过已经走过的方格

计算的结果,要用long long
*/
const int N = 1010;
int a[N][N];//读入的每个点的值
typedef long long LL;
LL f[N][N];
int n,m;

int main(){
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++){
        for(int j = 1;j <= m;j++){
            scanf("%d",&a[i][j]);
        }
    }

    /*
    DP求出走到每个点经过的数字和的最大值
    一列一列求,对于每一列而言,要求从上向下走到每个点经过的数字和的最大值
    也要求从下向上走到每个点经过的数字和的最大值
    */
    memset(f,-0x7f,sizeof(f));

    LL ma;//走到每个格子经过的数字和最大
    f[1][0] = 0;
    for(int j = 1;j <= m;j++){
        //从上向下求
        ma = -1e18;
        for(int i = 1;i <= n;i++){
            ma = max(ma,f[i][j-1]) + a[i][j];
            f[i][j] = max(f[i][j],ma);
        }

        //从下向上求
        ma = -1e18;
        for(int i = n;i >= 1;i--){
            ma = max(ma,f[i][j-1]) + a[i][j];
            f[i][j] = max(f[i][j],ma);
        }
    }

cout<<f[n][m];
return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值