自闭jpg. 就说说 PJ 吧。
TG炸的原因主要是因为PJ的炸裂以及T1……所以就直接分析根本原因了。
0x00
考前一天晚上。
在LH巨佬家吃了饭,前往离HF大约1小时车程的NK边上的酒店。
到酒店收拾了一会儿,制订了个小计划,复习了不少板子。
像最短路,RMQ,LCA,EXGCD,euler啥的,都过了遍手。
当晚心态还是比较稳的,期待。
考试早上。
跟着大部队朦朦胧胧地到了NK门口。但好像一到门口就来精神了(?
然后在金牌讲师的带领下拍了OI生涯第一张比赛集体照。
接下来就在操场上等着进考场呗,期间遇到了几个外校的熟人,寒暄了几句。
进考场。
场地布置有点像ACM。
我还特别的注意到了以一种奇怪的方式折叠在天花板上的篮球框。
键盘,机器都还挺顺手的,不过座位刚好对着一条桌子腿,不是很舒服。
然后在裁判员宣读考试注意事项的时候,重新在脑袋里幻灯片了一下考试策略。(是的,我后面想起我考前还想过这玩意的
0x01
赛中。
T1一如既往的水。就直接每次暴力跑比n小的最大的二次幂即可。
5分钟打完调完过掉大样例跑去看T2。
T2就很迷惑了,想了半天只想出一个插排解法,心态直接就炸掉。但没办法,只能打完插排去看后面的题。一切炸的开始。
插排思路: 每次更新时二分找到之前的第i个位置,满足 a[i - 1] < x < a[i],将其插入,时间复杂度:你以为是 (O(nlog_n)) ?hhhhhh其实是 (O(n^2))
T3感觉不可做,放掉,看T4。
T4第一眼好熟悉!马上反应是道dp,乱推了一堆通过单个二维转移的方程,没解出来。左看右看,考虑是不是最短路,结果直接随手把自己hack掉。最后打了个爆搜走人(我当时降智到直接没考虑记忆化。
再回去看T3,以为正解就是个暴力,后缀表达式乱搞交掉。
又跑去看T4,慌得要死,一直推到结束都没推出来。
最后五分钟T1刷了一个样例,感觉自己打错了,但其实是对的。于是魔改了一下,导致一中午都在担心自己好像没保存。
赛后。
一出考场就想起T4第二维两个循环转移。
一出考场就想起T2直接桶。。。(LH打树状数组过了,JC打对顶堆过了。
我谔谔,这次主要是心态炸锅了,太紧张了,导致一些白菜题没有切掉。
当然,后来反思也有考试策略有问题的成分。
估分:(100) (+) (85) (+) (30) (+) (20) (=) (235)。
实际能力极限:(100) (+) (100) (+) (30) (+) (100) (=) (330)。
以后做题还是应该再踏实一点,多做做思维题。
唉,好好复习NOIP吧。
0x02
题解部分
T1: (不再赘述。
#include <cstdio>
const int MAXN = 1e6 + 5;
int ans[MAXN], len = 0;
int main() {
int n;
scanf ("%d", &n);
while(n) {
if(n == 1) {
printf("-1");
return 0;
}
long long t = 1;
while(t <= n)
t <<= 1;
if(t > n)
t >>= 1;
n -= t;
ans[++len] = t;
}
for(int i = 1; i < len; i++)
printf("%d ", ans[i]);
printf("%d", ans[len]);
return 0;
}
T2: (数据只到600?那毋庸置疑是个桶a。不要像我那样没看到600,导致只打了插排。
每次更新新的分数的时候,累加相对应的桶。
然后每次查找的时候,从大往小暴力找,找到分数线的位置直接输出即可。
时间复杂度:(O(600n))
#include <cstdio>
inline int Max(int x, int y) {return x > y ? x : y;}
const int MAXN = 1e5 + 5;
const int MAXT = 605;
int a[MAXN], t[MAXT];
int main() {
int n, w;
scanf ("%d %d", &n, &w);
for(int i = 1; i <= n; i++) {
int x;
scanf ("%d", &x);
t[x]++;
int index = Max(1, i * w / 100), tot = 0, res = 0;
for(int j = 600; j >= 0; j--) {
tot += t[j];
if(tot >= index) {
res = j;
break;
}
}
printf("%d ", res);
}
return 0;
}
T3:(其实也不难想哦 可我还是不会
暴力就不提了吧。栈乱搞有30pt。个人认为这应该是最难的一道了。
至于正解,我们引入一个奇怪的字符树。
首先,你会发现整个表达式的取值只和一些特定的值有关,比如:
1 | (……) = 1;
0 & (……) = 0.
那么根据这个思路我们就可以考虑建树了嘛。实现看码。
时间复杂度:或许是 (O(n + q)) 吧。
#include <cstdio>
#include <stack>
#include <cstring>
using namespace std;
const int MAXN = 1e6 + 5;
int a[MAXN], len = 0;
// a[]一组多用,下标 1 ~ n 为数集,n ~ 为符号集。
int cnt[MAXN], t[MAXN][2], Index = 0, n = 0;
// cnt-方便输入的数组,t 树,index 输入建树的下标。
bool vis[MAXN], q[MAXN];
// vis 表示这个数是否会决定表达式的答案,q 表示取反标记。
// vis 中,true 表示它不是决定性的,false 表示是决定性的。
char s[MAXN];
int dfs(int u, int f) {
// 在建树过程中顺便算出初始表达式的值。
// u 表示遍历,f 表示遍历时的取反标记。
a[u] ^= f; // 取反。
if(u <= n)
return a[u]; // 如果属于数集直接返回。
a[u] ^= f;
// 如果不属于数集,先取反回来,进行字符操作。
int x = dfs(t[u][0], (f ^ q[t[u][0]]));
int y = dfs(t[u][1], (f ^ q[t[u][1]]));
// 左右儿子遍历。
if(a[u] == -2) { // 如果是 &。
if(x == 0)
vis[t[u][1]] = 1;
if(y == 0)
vis[t[u][0]] = 1;
int res = (x & y);
return res;
}
else { // 如果是 |。
if(x == 1)
vis[t[u][1]] = 1;
if(y == 1)
vis[t[u][0]] = 1;
int res = (x | y);
return res;
}
}
void update(int u) {
// 再扫一遍树。
// 我们之前只考虑了每个点的决定性情况。
// 但实际还应该考虑它的子树的决定性情况。
if (u <= n) return;
vis[t[u][0]] |= vis[u];
vis[t[u][1]] |= vis[u];
update(t[u][0]);
update(t[u][1]);
}
int main() {
while(1) {
scanf ("%s", s);
if(s[0] >= '0' && s[0] <= '9') {
for(int i = 0; i < strlen(s); i++)
n = (n << 3) + (n << 1) + s[i] - '0';
break;
}
if(s[0] == 'x') {
int x = 0;
for(int i = 1; i < strlen(s); i++)
x = (x << 3) + (x << 1) + s[i] - '0';
cnt[++len] = x;
}
else if(s[0] == '|')
cnt[++len] = -1;
else if(s[0] == '&')
cnt[++len] = -2;
else if(s[0] == '!')
cnt[++len] = -3;
}
// 输入与一些保存方式。
Index = n;
for(int i = 1; i <= n; i++)
scanf ("%d", &a[i]);
stack<int> num;
// 后缀表达式-数字栈。
for(int i = 1; i <= len; i++) {
if(cnt[i] >= 0) { // 是数字。
num.push(cnt[i]);
continue;
}
switch(cnt[i]) {
case -1: { // 是 |。
int x = num.top();
num.pop();
int y = num.top();
num.pop();
Index++;
num.push(Index);
a[Index] = -1;
t[Index][0] = x;
t[Index][1] = y;
break;
}
case -2: { // 是 &。
int x = num.top();
num.pop();
int y = num.top();
num.pop();
Index++;
num.push(Index);
a[Index] = -2;
t[Index][0] = x;
t[Index][1] = y;
break;
}
case -3: { // 是 !。
q[num.top()] ^= 1;
break;
}
}
}
int Q, ans = dfs(Index, q[Index]);
update(Index);
scanf ("%d", &Q);
for(int i = 1; i <= Q; i++) {
int x;
scanf ("%d", &x);
printf("%d
", vis[x] ? ans : (!ans));
}
return 0;
}
T4:(考场上降智了
一个数字三角形的变种。
其实就是需要多考虑一个从下往上走的情况,那就开两个dp更新嘛。
- dp[i][j][1] 表示从下往上或从左往右走到 (i, j);
- dp[i][j][0] 表示从上往下或从左往右走到 (i, j)。
这里指的是从上一步到这一步的状态。显然,答案就在 Max(dp[i][j][0], dp[i][j][1]) 里面。
然后就用类数字三角形的思路跑即可。
时间复杂度:(O(nm))
#include <cstdio>
#include <cstring>
using namespace std;
typedef long long LL;
inline LL Max(LL x, LL y) {return x > y ? x : y;}
const int MAXN = 1005;
const int INF = 0x3f3f3f3f;
LL dp[MAXN][MAXN][2];
int a[MAXN][MAXN];
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 ("%d", &a[i][j]);
memset(dp, -INF, sizeof dp);
dp[1][0][0] = 0;
for(int j = 1; j <= m; j++) {
for(int i = 1; i <= n; i++)
dp[i][j][0] = Max(dp[i - 1][j][0], Max(dp[i][j - 1][1], dp[i][j - 1][0])) + a[i][j];
for(int i = n; i >= 1; i--)
dp[i][j][1] = Max(dp[i + 1][j][1], Max(dp[i][j - 1][1], dp[i][j - 1][0])) + a[i][j];
// 因为不能重复走所以 dp[i][j][0] 不能被 dp[i - 1][j][1] 更新,同理 dp[i][j][1] 不能被 dp[i + 1][j][0]更新。
// 那么就只需要将走到左边点的最优解与上面或下面可以用于更新的最优解比最大即可。
}
printf("%lld
", dp[n][m][0]);
return 0;
}
就当是重新回到起跑线上,凡是过去,皆为序章。