T1 优秀的拆分
题目描述
一般来说,一个正整数可以拆分成若干个正整数的和。
例如, 1 = 1 1=1 1=1, 10 = 1 + 2 + 3 + 4 10=1+2+3+4 10=1+2+3+4 等。对于正整数 n n n 的一种特定拆分,我们称它为“优秀的”,当且仅当在这种拆分下, n n n 被分解为了若干个不同的 2 2 2 的正整数次幂。注意,一个数 x x x 能被表示成 2 2 2 的正整数次幂,当且仅当 x x x 能通过正整数个 2 2 2 相乘在一起得到。
例如, 10 = 8 + 2 = 2 3 + 2 1 10=8+2=2^3+2^1 10=8+2=23+21 是一个优秀的拆分。但是, 7 = 4 + 2 + 1 = 2 2 + 2 1 + 2 0 7=4+2+1=2^2+2^1+2^0 7=4+2+1=22+21+20 就不是一个优秀的拆分,因为 1 1 1 不是 2 2 2 的正整数次幂。
现在,给定正整数 n n n,你需要判断这个数的所有拆分中,是否存在优秀的拆分。若存在,请你给出具体的拆分方案。
输入格式
输入只有一行,一个整数
n
n
n,代表需要判断的数。
输出格式
如果这个数的所有拆分中,存在优秀的拆分。那么,你需要从大到小输出这个拆分中的每一个数,相邻两个数之间用一个空格隔开。可以证明,在规定了拆分数字的顺序后,该拆分方案是唯一的。
若不存在优秀的拆分,输出 − 1 -1 −1。
输入输出样例
输入 #1
6
输出 #1
4 2
输入 #2
7
输出 #2
-1
说明/提示
样例 1 解释
6
=
4
+
2
=
2
2
+
2
1
6=4+2=2^2+2^1
6=4+2=22+21 是一个优秀的拆分。注意,
6
=
2
+
2
+
2
6=2+2+2
6=2+2+2 不是一个优秀的拆分,因为拆分成的
3
3
3 个数不满足每个数互不相同。
数据规模与约定
- 对于 20 % 20\% 20% 的数据, n ≤ 10 n \le 10 n≤10。
- 对于另外 20 % 20\% 20% 的数据,保证 n n n 为奇数。
- 对于另外 20 % 20\% 20% 的数据,保证 n n n 为 2 2 2 的正整数次幂。
- 对于 80 % 80\% 80% 的数据, n ≤ 1024 n \le 1024 n≤1024。
- 对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 1 × 1 0 7 1 \le n \le 1 \times 10^7 1≤n≤1×107。
T1 分析
T1 送分题,一个整数可以转换成二进制形式,那二进制下如果存在
1
1
1 则将这个
1
1
1 对应的
2
2
2 的幂次输出即可
但是题目要求的是正整数幂次,所以若存在
2
0
2^0
20 则输出
−
1
-1
−1,那其实就是奇数的时候输出
−
1
-1
−1
#include <iostream>
#include <cstdio>
using namespace std;
int main (){
int n;
scanf("%d", &n);
if (n % 2){
puts("-1");
} else {
int flag = 0;
for (int i = 30; i >= 1; --i){
if (n & (1 << i)){
if (flag){
printf(" ");
}
flag = 1;
printf("%d", (1 << i));
}
}
}
return 0;
}
T2 直播获奖
题目描述
NOI2130 即将举行。为了增加观赏性,CCF 决定逐一评出每个选手的成绩,并直播即时的获奖分数线。本次竞赛的获奖率为
w
%
w\%
w%,即当前排名前
w
%
w\%
w% 的选手的最低成绩就是即时的分数线。
更具体地,若当前已评出了 p p p 个选手的成绩,则当前计划获奖人数为 max ( 1 , ⌊ p ∗ w % ⌋ ) \max(1, \lfloor p * w \%\rfloor) max(1,⌊p∗w%⌋),其中 w w w 是获奖百分比, ⌊ x ⌋ \lfloor x \rfloor ⌊x⌋ 表示对 x x x 向下取整, max ( x , y ) \max(x,y) max(x,y) 表示 x x x 和 y y y 中较大的数。如有选手成绩相同,则所有成绩并列的选手都能获奖,因此实际获奖人数可能比计划中多。
作为评测组的技术人员,请你帮 CCF 写一个直播程序。
输入格式
第一行有两个整数
n
,
w
n, w
n,w。分别代表选手总数与获奖率。
第二行有
n
n
n 个整数,依次代表逐一评出的选手成绩。
输出格式
只有一行,包含
n
n
n 个非负整数,依次代表选手成绩逐一评出后,即时的获奖分数线。相邻两个整数间用一个空格分隔。
输入输出样例
输入 #1
10 60
200 300 400 500 600 600 0 300 200 100
输出 #1
200 300 400 400 400 500 400 400 300 300
输入 #2
10 30
100 100 600 100 100 100 100 100 100 100
输出 #2
100 100 600 600 600 600 100 100 100 100
说明/提示
样例 1 解释
数据规模与约定
各测试点的
n
n
n 如下表:
提示
在计算计划获奖人数时,如用浮点类型的变量(如 C/C++ 中的 float 、 double,Pascal 中的 real 、 double 、 extended 等)存储获奖比例
w
%
w\%
w%,则计算
5
×
60
%
5 \times 60\%
5×60% 时的结果可能为
3.000001
3.000001
3.000001,也可能为
2.999999
2.999999
2.999999,向下取整后的结果不确定。因此,建议仅使用整型变量,以计算出准确值。
T2 分析
题意其实很简单,就是动态的查询前
p
p
p 个人中排名为
k
k
k 的人成绩是多少,乍一看是个平衡树模板,但是这道题的重点在于每个选手成绩均不超过
600
600
600,而且普及组 T2 想想也知道不可能那么难…
所以其实只需要直接桶排序即可,没什么难度,复杂度
O
(
n
m
)
,
m
=
600
O(nm),m = 600
O(nm),m=600
#include <iostream>
#include <cstdio>
using namespace std;
int a[1000];
int main (){
int n, w;
int flag = 0;
scanf("%d%d", &n, &w);
for (int i = 1; i <= n; ++i){
int x;
scanf("%d", &x);
a[x]++;
int rank = max(1, i * w / 100);
int now = 0;
for (int i = 600; i >= 0; --i){
now += a[i];
if (now >= rank){
if (flag){
printf(" ");
}
flag = 1;
printf("%d", i);
break;
}
}
}
return 0;
}
T3 表达式
题目描述
小 C 热衷于学习数理逻辑。有一天,他发现了一种特别的逻辑表达式。在这种逻辑表达式中,所有操作数都是变量,且它们的取值只能为
0
0
0 或
1
1
1,运算从左往右进行。如果表达式中有括号,则先计算括号内的子表达式的值。特别的,这种表达式有且仅有以下几种运算:
- 与运算:
a & b
。当且仅当 a a a 和 b b b 的值都为 1 1 1 时,该表达式的值为 1 1 1。其余情况该表达式的值为 0 0 0。 - 或运算:
a | b
。当且仅当 a a a 和 b b b 的值都为 0 0 0 时,该表达式的值为 0 0 0。其余情况该表达式的值为 1 1 1。 - 取反运算:
!a
。当且仅当 a a a 的值为 0 0 0 时,该表达式的值为 1 1 1。其余情况该表达式的值为 0 0 0。
小 C 想知道,给定一个逻辑表达式和其中每一个操作数的初始取值后,再取反某一个操作数的值时,原表达式的值为多少。
为了化简对表达式的处理,我们有如下约定:
表达式将采用后缀表达式的方式输入。
后缀表达式的定义如下:
- 如果 E E E 是一个操作数,则 E E E 的后缀表达式是它本身。
- 如果 E E E 是 E 1 op E 2 E_1~\texttt{op}~E_2 E1 op E2 形式的表达式,其中 op ~\texttt{op} op 是任何二元操作符,且优先级不高于 E 1 E_1 E1 中括号外的操作符,则 E E E 的后缀式为 E 1 ′ E 2 ′ op E_1' E_2' \texttt{op} E1′E2′op 其中 E 1 ′ E_1' E1′ 、 E 2 ′ E_2' E2′ 分别为 E 1 E_1 E1 、 E 2 E_2 E2 的后缀式。
- 如果 E E E 是 E 1 E_1 E1 形式的表达式,则 E 1 E_1 E1 的后缀式就是 E E E 的后缀式。
同时为了方便,输入中:
- 与运算符(&)、或运算符(|)、取反运算符(!)的左右均有一个空格,但表达式末尾没有空格。
- 操作数由小写字母
x
x
x 与一个正整数拼接而成,正整数表示这个变量的下标。例如:
x10
,表示下标为 10 10 10 的变量 x 10 x_{10} x10 。数据保证每个变量在表达式中出现恰好一次。
输入格式
第一行包含一个字符串
s
s
s,表示上文描述的表达式。
第二行包含一个正整数
n
n
n,表示表达式中变量的数量。表达式中变量的下标为
1
,
2
,
⋯
,
n
1,2, \cdots , n
1,2,⋯,n。
第三行包含
n
n
n 个整数,第
i
i
i 个整数表示变量
x
i
x_i
xi 的初值。
第四行包含一个正整数
q
q
q,表示询问的个数。
接下来
q
q
q 行,每行一个正整数,表示需要取反的变量的下标。注意,每一个询问的修改都是临时的,即之前询问中的修改不会对后续的询问造成影响。
数据保证输入的表达式合法。变量的初值为
0
0
0 或
1
1
1。
输出格式
输出一共有
q
q
q 行,每行一个
0
0
0 或
1
1
1,表示该询问下表达式的值。
输入输出样例
输入 #1
x1 x2 & x3 |
3
1 0 1
3
1
2
3
输出 #1
1
1
0
输入 #2
x1 ! x2 x4 | x3 x5 ! & & ! &
5
0 1 0 1 1
3
1
3
5
输出 #2
0
1
1
说明/提示
样例 1 解释
该后缀表达式的中缀表达式形式为
(
x
1
&
x
2
)
∣
x
3
(x_1 \& x_2) | x_3
(x1&x2)∣x3 。
对于第一次询问,将
x
1
x_1
x1 的值取反。此时,三个操作数对应的赋值依次为
0
0
0,
0
0
0,
1
1
1。原表达式的值为
(
0
&
0
)
∣
1
=
1
(0\&0)|1=1
(0&0)∣1=1。
对于第二次询问,将
x
2
x_2
x2 的值取反。此时,三个操作数对应的赋值依次为
1
1
1,
1
1
1,
1
1
1。原表达式的值为
(
1
&
1
)
∣
1
=
1
(1\&1)|1=1
(1&1)∣1=1。
对于第三次询问,将
x
3
x_3
x3 的值取反。此时,三个操作数对应的赋值依次为
1
1
1,
0
0
0,
0
0
0。原表达式的值为
(
1
&
0
)
∣
0
=
0
(1\&0)|0=0
(1&0)∣0=0。
样例 2 解释
该表达式的中缀表达式形式为
(
!
x
1
)
&
(
!
(
(
x
2
∣
x
4
)
&
(
x
3
&
(
!
x
5
)
)
)
)
(!x_1)\&(!((x_2|x_4)\&(x_3\&(!x_5))))
(!x1)&(!((x2∣x4)&(x3&(!x5))))
数据规模与约定
对于
20
%
20\%
20% 的数据,表达式中有且仅有与运算(&)或者或运算(|)。
对于另外
30
%
30\%
30% 的数据,
∣
s
∣
≤
1000
|s| \le 1000
∣s∣≤1000,
q
≤
1000
q \le 1000
q≤1000,
n
≤
1000
n \le 1000
n≤1000。
对于另外
20
%
20\%
20% 的数据,变量的初值全为
0
0
0 或全为
1
1
1。
对于
100
%
100\%
100% 的数据,
1
≤
∣
s
∣
≤
1
×
1
0
6
1 \le |s| \le 1 \times 10^6
1≤∣s∣≤1×106 ,
1
≤
q
≤
1
×
1
0
5
1 \le q \le 1 \times 10^5
1≤q≤1×105 ,
2
≤
n
≤
1
×
1
0
5
2 \le n \le 1 \times 10^5
2≤n≤1×105
其中,
∣
s
∣
|s|
∣s∣ 表示字符串
s
s
s 的长度。
T3 分析
全场最难题,确实挺难的…需要在考场上如果要
100
%
100\%
100% 那就意味着其他三题要出的够快,才会有足够的时间来思考这道题。
所以比赛的时候一定要注意先把题目都看完,不要只是从前往后去做题然后卡在这道题上发现来不及做第四题。
所以这道题在考场上的建议是先做第四题再来看时间摸部分分还是去思考正确解法,显然这道题如果没有 !
非运算的话其实难度并不高,以及还存在
20
%
20\%
20% 是全
0
0
0 或者全
1
1
1 的情况,都是好拿分数的地方。
上面也说了,其实最复杂的就是
!
!
! 非这个操作,然后注意关键点是题意中说明的 每个变量在表达式中出现恰好一次
这句话是在反过来提示一件事——有些变量的改变会导致答案发生变化,而有些变量的改变则不会对答案产生影响。
所以这道题的正确思考方向应该是如何去找到那些会使得答案发生变化的变量,那么其实后面的询问也就很简单了,如果
x
x
x 会使得答案发生变化,那么答案就是
!
a
n
s
!ans
!ans,否则答案维持不变即为
a
n
s
ans
ans
那么什么样的变量会导致答案发生变化呢?
若是只有 &
和 |
两种操作,那么显而易见的,如果是 1&x
和 0|x
,那么
x
x
x 是对答案有影响的,而如果是 0&x
和 1|x
那
x
x
x 的值则不会对答案造成影响
后缀表达式其实可以画出一棵表达式树,那么我们就可以发现,若是对一个操作符的左右两棵子树来说,如果当前操作符是 &
则意味着若一边子树的值为
0
0
0,那么另一边子树内的所有值的修改对答案都不会造成影响,|
也同理
所以如果没有 !
这个操作,那么基本就已经完成了,我们只要按照后缀表达式建出这颗表达式树,然后对每个节点进行标记,标记这个节点的修改是否会对答案造成影响即可
那么剩下的问题就来了,!
取反这个操作在这道题中有可能会对一串连续的操作全部进行取反,那么这里需要用到的就是 德·摩根定律
,其实内容也很简单
!(P & Q) = (! P) | (! Q)
!(P | Q) = (! P) & (! Q)
从德摩根定律中可以看到,其实对一个括号内的表达式取反,相当于对其中的 值
和 运算符
均进行一次取反,对运算符的取反即 |
和 &
之间的互换
那么这里其实也同样可以在这颗表达式树上解决,对 !
进行标记,!
的子树下均需要进行取反直到中间,而若是 !
下碰到了另一个 !
则抵消效果,这个很好理解,两次 !
等于没有操作
所以整体分三步
- 读取表达式建立表达式树
- 对表达式树进行取反处理同时计算出表达式的值,并且对上述不影响答案变化的子树根节点进行标记
- 从根节点开始将不影响答案变化的标记下放
那么这道题就完成了,剩下的无非就是代码如何实现,细节部分注释在代码里了
#include <iostream>
#include <cstring>
#include <stack>
#include <cstdio>
using namespace std;
char s[1000005];
int a[1000005];
int son[1000005][2];
int flag[1000005];
int check[1000005];
int n, q, root;
stack<int> b;
int dfs(int now, int f) {
a[now] ^= f; // 先对当前这个节点取反
if (now <= n) { // 变量节点即为叶子节点
return a[now];
}
// 分别计算两棵子树并且 取反标记 ^
int x = dfs(son[now][0], f ^ flag[son[now][0]]);
int y = dfs(son[now][1], f ^ flag[son[now][1]]);
if (a[now] == 2) { // &
if (x == 0){
check[son[now][1]] = 1;
}
if (y == 0) {
check[son[now][0]] = 1;
}
return x & y;
} else { // |
if (x == 1) {
check[son[now][1]] = 1;
}
if (y == 1) {
check[son[now][0]] = 1;
}
return x | y;
}
}
void dfss(int now) {
if (now <= n){
return;
}
check[son[now][0]] |= check[now];
check[son[now][1]] |= check[now];
dfss(son[now][0]);
dfss(son[now][1]);
}
void add(int now){
int x = b.top();b.pop();
int y = b.top();b.pop();
root++;
b.push(root);
a[root] = now;
son[root][0] = x;
son[root][1] = y;
}
// 0 1 2 3 分别表示 0 1 & |
int main() {
gets(s);
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
int len = strlen(s);
root = n; // 所有操作都增加到 a 数组后面
for (int i = 0; i < len; i += 2) {
if (s[i] == 'x') {
int x = 0;
i++;
while (s[i] != ' ') {
x = x * 10 + s[i] - '0';
i++;
}
i--;
b.push(x);
} else if (s[i] == '!'){
int x = b.top();
flag[x] ^= 1; // 标记一下这次 ! 是从表达式树的哪个节点开始的,^ 是防止连续取反
}
else if (s[i] == '&') {
add(2);
} else if (s[i] == '|') {
add(3);
}
}
int ans = dfs(root, flag[root]);
dfss(root);
scanf("%d", &q);
while (q--) {
int x;
scanf("%d", &x);
if (check[x]){
printf("%d\n", ans);
} else {
printf("%d\n", !ans);
}
}
return 0;
}
T4 方格取数
题目描述
设有
n
×
m
n \times m
n×m 的方格图,每个方格中都有一个整数。现有一只小熊,想从图的左上角走到右下角,每一步只能向上、向下或向右走一格,并且不能重复经过已经走过的方格,也不能走出边界。小熊会取走所有经过的方格中的整数,求它能取到的整数之和的最大值。
输入格式
第一行有两个整数
n
,
m
n, m
n,m。
接下来 n n n 行每行 m m m 个整数,依次代表每个方格中的整数。
输出格式
一个整数,表示小熊能取到的整数之和的最大值。
输入输出样例
输入 #1
3 4
1 -1 3 2
2 -1 4 -1
-2 2 -3 -1
输出 #1
9
输入 #2
2 5
-1 -1 -3 -2 -7
-2 -1 -4 -1 -2
输出 #2
-10
说明/提示
样例 1 解释
样例 2 解释
数据规模与约定
- 对于 20 % 20\% 20% 的数据, n , m ≤ 5 n, m \le 5 n,m≤5。
- 对于 40 % 40\% 40% 的数据, n , m ≤ 50 n, m \le 50 n,m≤50
- 对于 70 % 70\% 70% 的数据, n , m ≤ 300 n, m \le 300 n,m≤300
- 对于 100 % 100\% 100% 的数据, 1 ≤ n , m ≤ 1 0 3 1 \le n,m \le 10^3 1≤n,m≤103 方格中整数的绝对值不超过 1 0 4 10^4 104
T4分析
20 % 20\% 20% 的数据暴力搜索都可以没啥问题,但是这道题的难度却要比第三题来的低,所以可以看得出来做题一定要有技巧,不能出现比完赛说第四题还没怎么看或者来不及写的情况
40 % 40\% 40% 应该是搜索的优化
70
%
70\%
70% 其实很容易看出来,这道题是左上角走到右下角,有三个方向,如果去掉向上这个方向,那就是一个很基础的
d
p
dp
dp,所以这道题放在第四题的位置但是难度却又不高,是一个题意非常明显的
d
p
dp
dp
那么接着来看一下如何实现这个
d
p
dp
dp,首先我们可以发现:向上、向下、向右,其中向右是一个比较特殊的情况,因为只要我们向右了,就没有办法再回到前一列了,而如果我们向下或者向上,则依旧可以在后面的列中回到同一行。
所以这
70
%
70\%
70% 的难点其实就在这里,这里打破了我们常规理解的
f
[
i
]
[
j
]
f[i][j]
f[i][j] 表示起点到达第
i
i
i 行第
j
j
j 列的最大值,这里我们的定义应该是以列优先的形式进行的
所以
f
[
i
]
[
j
]
f[i][j]
f[i][j] 表示从起点到达第 i 列,第 j 行
的最大值,只要想到这个状态表示了,那状态转移就很容易想到了。
对于某一个位置
(
j
,
i
)
(j,i)
(j,i) 来说,这个点可以由第
i
−
1
i - 1
i−1 列中任意一个位置
(
k
,
i
−
1
)
(k,i-1)
(k,i−1) 通过连续向下或者连续向上移动到
(
j
,
i
−
1
)
(j,i-1)
(j,i−1) 然后向右移动到达,所以可以得到方程
f [ i ] [ j ] = max 1 ≤ k ≤ n ( f [ i − 1 ] [ k ] + s u m [ k ] [ j ] + a [ j ] [ i ] ) f[i][j] = \max\limits_{1\leq k \leq n}(f[i-1][k] + sum[k][j] + a[j][i]) f[i][j]=1≤k≤nmax(f[i−1][k]+sum[k][j]+a[j][i])
这里
s
u
m
[
k
]
[
j
]
sum[k][j]
sum[k][j] 表示从
(
k
,
i
−
1
)
(k,i-1)
(k,i−1) 到
(
j
,
i
−
1
)
(j,i-1)
(j,i−1) 的和
但是这个
d
p
dp
dp 的复杂度显然需要
O
(
n
2
m
)
O(n^2m)
O(n2m)
那么
100
%
100\%
100% 就是思考一下如何优化,这个优化方向其实很简单,无非就是优化成
O
(
n
m
)
O(nm)
O(nm),那么考虑如何优化掉一个
n
n
n
那很容易想到需要优化掉的就是这个枚举
k
k
k 求
m
a
x
(
f
[
i
−
1
]
[
k
]
+
s
u
m
[
k
]
]
[
j
]
)
max(f[i-1][k] + sum[k]][j])
max(f[i−1][k]+sum[k]][j]) 的过程
所以其实只要在枚举的过程中顺便记录一下前缀最大值
n
o
w
now
now 即可
那么对于枚举到
j
j
j 时
n
o
w
=
m
a
x
(
f
[
i
−
1
]
[
j
]
,
n
o
w
+
a
[
j
−
1
]
[
i
]
)
now = max(f[i-1][j], now + a[j-1][i])
now=max(f[i−1][j],now+a[j−1][i]),即要么直接从
(
j
,
i
−
1
)
(j,i-1)
(j,i−1) 向右一步走到
(
j
,
i
)
(j,i)
(j,i),要么就是从之前的最大值
n
o
w
now
now 向下一步走到
(
j
,
i
−
1
)
(j,i-1)
(j,i−1) 再向右走到
(
j
,
i
)
(j,i)
(j,i)
但是这里要注意,因为是
1
∼
n
1\sim n
1∼n 枚举的,所以其实这里表示的都是从上往下走,但是显然题目还允许你从下往上走,所以倒过来一模一样再扫一遍即可,复杂度
O
(
n
m
)
O(nm)
O(nm)
这里要注意,答案应该是 a n s = max 1 ≤ i ≤ n ( f [ m ] [ i ] + s u m [ i ] [ m ] ) ans = \max\limits_{1\leq i \leq n}(f[m][i] + sum[i][m]) ans=1≤i≤nmax(f[m][i]+sum[i][m])
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const long long INF = 0x3f3f3f3f3f3f3f3f;
long long a[1010][1010];
long long f[1010][1010];
int main (){
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i){
for (int j = 1; j <= m; ++j){
scanf("%lld", &a[i][j]);
}
}
memset(f, -INF, sizeof(f));
f[1][1] = a[1][1];
for (int i = 2; i <= m; ++i){
long long now = -INF;
for (int j = 1; j <= n; ++j){
now = max(f[i - 1][j], now + a[j][i - 1]);
f[i][j] = now + a[j][i];
}
now = -INF;
for (int j = n; j >= 1; --j){
now = max(f[i - 1][j], now + a[j][i - 1]);
f[i][j] = max(f[i][j], now + a[j][i]);
}
}
long long ans = f[m][n], s = a[n][m];
for (int i = n - 1; i >= 1; --i){
ans = max(ans, f[m][i] + s);
s += a[i][m];
}
printf("%lld\n", ans);
return 0;
}