总览
本文同步发表与:
- 洛谷:https://www.luogu.com.cn/article/hdzdhnif。
- 博客园:<>。
打得不好,在赛时只做了 A 题。昨晚的睡眠使我刚好处于困和不困的叠加态,导致想题的时候脑子极乱。
A:Gym 103430F。
B:CF 578B。
C:CF 1407D。
D:洛谷 P11122。
E:CF 1208D。
A - Gym 103430F - X-Magic Pair
签到题。
设两数为 \(a,b\),默认 \(a > b\)(小于就 swap
)。
那么差就是 \(a-b\)。考虑用 \(a-b\) 代替 \(b\),那么就变为 \(a,a-b\),这两个数相减之后就又变成了 \(a,b\)。是无用功。
所以用 \(a-b\) 代替 \(a\)。此时两数变为 \(a-b,b\)。
如果 \(a-b < b\),那么 swap
,重新定义 \(a,b\),重复上面的讨论。
如果 \(a-b > b\),那么接着相减就变为 \(a-2b\)。用 \(a-2b\) 代替 \(a-b\),接着讨论……
你会发现,这个操作实际上就是不断让 \(a-b\),直到 \(a < b\) 之后交换。
因此这个操作相当于 \(a \gets a \bmod b\),然后交换二者。
那么最终答案判定怎么判?当 \(b \le x \le a\) 时,看 \(a\) 能否减去若干个 \(b\) 使得 \(a = x\)。
也就是说,是否存在一个正整数 \(k\),使得 \(a-kb = x\)。化简得 \(k = \frac{a-x}{b}\)。
只要右边那一坨是正整数就有解。
上面默认 \(a \not = b\)。如果等于,要特判,不然会死循环。
#include<bits/stdc++.h>
using namespace std;
long long t;
long long a,b,x;
int main(){scanf("%lld",&t);while(t--){scanf("%lld%lld%lld",&a,&b,&x);if(a < b){swap(a,b);}if(x > a){puts("NO");continue;}while(!(b <= x && x <= a)){a -= ((a-1)/b)*b;if(a < b){swap(a,b);}if(a == b){break;}}if(x == a || x == b || (a-x)%b == 0){puts("YES");}else{puts("NO");}}return 0;
}
/*
Traveling in the nights you've left me in.
I feel you in the last blow of wind.
Even nowhere I can find you out.
The answer is not far off now.This journey of ours has been bittersweet.
Close my eyes wondering what you would have dreamed.
If you were here standing next to me,
Would you know, how I feel,
And see what I see?Know that I'll always try.
Finding you rhythm and rhyme.
Though the nights are long and dark, I'll see you shining bright.
And no matter where you are, you'll come with me this far.
Showing the way when all else falls apart~
*/
B - Codeforces 578B - "Or" Game
考场上一眼看上去是 DP,但是或运算不满足最优子结构特征,也就是局部最优解不等于全局最优解。因此 DP 假了。
实际上是贪心。这 \(k\) 次乘法一定要用在一个数身上。因为是或,所以要让有效位数尽可能多。又因为 \(x \ge 2\),所以实际上每次乘法都会向右至少移 1 位。所以你肯定得搁一个数往右移。
所以 \(O(n)\) 遍历每个数,枚举如果这个数乘 \(k\) 次 \(x\) 之后,总答案是多少,最后取 \(\max\)。具体计算时可以用前缀和思想,维护前缀或与后缀或即可。
#include<bits/stdc++.h>
using namespace std;
long long n,m,k,ans = 0;
long long num[200050];
long long f[200050],b[200050];
long long slowpow(long long x,long long y){long long res = x;while(y--){res *= k;}return res;
}
int main(){scanf("%lld%lld%lld",&n,&m,&k);for(long long i = 1; i <= n; i++){scanf("%lld",&num[i]);f[i] = num[i]|f[i-1];}for(long long i = n; i >= 1; i--){b[i] = num[i]|b[i+1];}for(long long i = 1; i <= n; i++){ans = max(ans,f[i-1]|b[i+1]|slowpow(num[i],m));}printf("%lld",ans);return 0;
}
C - Codeforces 1407D - Discrete Centrifugal Jumps
既然连 WYR 小朋友都做出来了,我怎么不会做呢……
考场上脑子极乱,思路都是一样的,但是脑抽了。
具体的,我想:“设在 \(i\) 之前且第一个比 \(i\) 高的楼是 \(j\)”,然后想了半天又想:“万一有楼在 \(i \sim j\) 之间且比 \(i\) 高比 \(j\) 低呢……”
可见我是智障。
考虑朴素 DP。设 \(dp_i\) 为到第 \(i\) 号楼的最少步数。初始化 \(dp_1 \gets 0\),其余全部为 \(\infty\)。
那么转移就是题目中描述的那样,分三种情况转移。
暴力代码:
memset(dp,0x3f,sizeof(int)*(n+10));
dp[1] = 0;
for(int i = 2; i <= n; i++){static int Min,Max;Min = inf,Max = -inf;dp[i] = dp[i-1]+1;for(int j = i-1; j >= 1; j--){if(max(num[i],num[j]) < Min){dp[i] = min(dp[i],dp[j]+1);}if(min(num[i],num[j]) > Max){dp[i] = min(dp[i],dp[j]+1);}Min = min(Min,num[j]);Max = max(Max,num[j]);}
}
先考虑 \(\max{h_{i+1},h_{i+2},\dots,h_{j-1}} < \min{h_i,h_j}\) 的情况。另一种情况一样。
考虑一个 \(j\),如果 \(\max{h_{i+1},h_{i+2},\dots,h_{j-1}} > \min{h_i,h_j}\),这个 \(j\) 就失去了贡献资格。最后有资格的 \(j\) 按照坐标排下来一定是严格单调下降的。
手玩一下就会发现这个过程跟单调栈一模一样。当一个新元素有贡献资格时,原本可以贡献的,但是高度小于等于它的元素就失去了资格。这跟弹出栈顶的操作是同一个思想。
而这些弹出的元素都可以向准备加入的元素转移。同时第一个高度大于等于它的元素也可以向它转移。
所以维护一个单调栈,栈内存有资格贡献的元素,然后维护单调下降 / 单调上升的元素集合即可。
#include<bits/stdc++.h>
using namespace std;
long long n;
long long num[300050],dp[300050];
stack<long long> Max,Min;
int main(){scanf("%lld",&n);for(long long i = 1; i <= n; i++){scanf("%lld",&num[i]);}memset(dp,0x3f,sizeof(long long)*(n+10));dp[1] = 0;Max.push(1),Min.push(1);for(long long i = 2; i <= n; i++){dp[i] = dp[i-1]+1;while(Max.size() && num[i] >= num[Max.top()]){if(num[i] != num[Max.top()] && (int)Max.size() > 1){Max.pop();dp[i] = min(dp[i],dp[Max.top()]+1);}else{Max.pop();}}Max.push(i);while(Min.size() && num[i] <= num[Min.top()]){if(num[i] != num[Min.top()] && (int)Min.size() > 1){Min.pop();dp[i] = min(dp[i],dp[Min.top()]+1);}else{Min.pop();}}Min.push(i);}printf("%lld",dp[n]);return 0;
}
D - 洛谷 P11122 - 表格游戏
折半搜索,新知识。
发现删除顺序与最终答案无关。
考虑暴力。用状压表示删除情况。枚举删哪几行,然后枚举删哪几列。最坏情况两者各有 \(2^{15}\) 种情况,总复杂度为 \(O(2^{h+w})\)。
考虑折半搜索。这个算法的思想为把搜索区间分成两半,每一半单独搜索,然后用高效的方法合并结果。
对于这道题,我们可以先枚举删哪几行,然后先枚举只删 \(1 \sim \lfloor \frac{w}{2} \rfloor\) 列的情况,再枚举只删 \(\lceil \frac{w}{2} \rceil \sim w\) 列的情况。最后合并二者的结果,判断是否有解。
这个方法优化了时间复杂度,从 \(O(2^{h+w})\) 优化到了 \(O(2^{h+ \frac{w}{2}})\)。可过。
#include<bits/stdc++.h>
using namespace std;
long long n,m,q;
long long num[20][20],sum[20];
long long state1;
unordered_map<long long,long long> state2;
vector<pair<long long,long long>> ans;
void dfs1(long long now,long long state,long long add){if(now == (m>>1)+1){state2[add] = state;return;}dfs1(now+1,state,add);dfs1(now+1,state|(1<<(now-1)),add+sum[now]);return;
}
void dfs2(long long now,long long state,long long add){if(now == m>>1){if(state2.count(q-add)){puts("YES");for(long long i = 0; i < n; i++){if(state1&(1<<i)){ans.push_back({1,i+1});}}for(long long i = 0; i < m; i++){if(!(state2[q-add]&(1<<i)) && !(state&(1<<i))){ans.push_back({2,i+1});}}printf("%lld\n",(long long)ans.size());for(auto i:ans){printf("%lld %lld\n",i.first,i.second);}exit(0);}return;}dfs2(now-1,state,add);dfs2(now-1,state|(1<<(now-1)),add+sum[now]);
}
int main(){scanf("%lld%lld",&n,&m);for(long long i = 1; i <= n; i++){for(long long j = 1; j <= m; j++){scanf("%lld",&num[i][j]);}}scanf("%lld",&q);for(long long i = 0; i < (1<<n); i++){for(long long j = 1; j <= m; j++){sum[j] = 0;for(long long k = 1; k <= n; k++){if(!(i&(1<<(k-1)))){sum[j] += num[k][j];}}}state1 = i;state2.clear();dfs1(1,0,0);dfs2(m,0,0);}puts("NO");return 0;
}
E - Codeforces 1208D - Restore Permutation
没想到最后一题反而更简单。可惜啊,没看。
首先,最后一个 \(0\) 一定是 \(1\)。因为在它前面没有比它更小的数了,而它后面的所有数都有比自身更小的数。
那么 \(1\) 的位置确定,不妨设其位置为 \(i\)。那么 \(1\) 会对 \(i+1 \sim n\) 的所有数产生贡献。所以把 \(i+1 \sim n\) 的所有数减去 \(1\),然后删掉 \(i\) 位置的这个数。
然后讨论 \(2\)。跟 \(1\) 的思路一样,讨论完 \(1\) 之后,最后一个 \(0\) 就是 \(2\)。以此类推。
所以我们需要一个可区间修改,区间查询最小值的数据结构。
太好了,是线段树,我们有救了!
至于如何知道最后一个 \(0\) 的具体下标,可以用二分。所以复杂度应该是双 \(\log\)。
至于如何删掉这个数,其实不用真的删,赋值为 \(\infty\) 即可。
注意开 long long
,而且 0x3f3f3f3f
不够用。
#include<bits/stdc++.h>
using namespace std;
const long long inf = 0x3f3f3f3f3f3f3f3f;
long long n;
long long num[200050],ans[200050];
long long tree[200050<<2],tag[200050<<2];
long long ls(long long x){return x<<1;
}
long long rs(long long x){return x<<1|1;
}
void push_up(long long p){tree[p] = min(tree[ls(p)],tree[rs(p)]);return;
}
void build(long long p,long long pl,long long pr){tree[p] = inf,tag[p] = 0;if(pl == pr){tree[p] = num[pl];return;}long long mid = pl+((pr-pl)>>1);build(ls(p),pl,mid);build(rs(p),mid+1,pr);push_up(p);return;
}
void addtag(long long p,long long opt){tree[p] -= opt;tag[p] += opt;return;
}
void push_down(long long p){if(tag[p]){addtag(ls(p),tag[p]);addtag(rs(p),tag[p]);tag[p] = 0;}return;
}
void modify(long long p,long long pl,long long pr,long long l,long long r,long long opt){if(l <= pl && pr <= r){addtag(p,opt);return;}long long mid = pl+((pr-pl)>>1);push_down(p);if(l <= mid){modify(ls(p),pl,mid,l,r,opt);}if(mid < r){modify(rs(p),mid+1,pr,l,r,opt);}push_up(p);return;
}
long long query(long long p,long long pl,long long pr,long long l,long long r){if(l <= pl && pr <= r){return tree[p];}long long mid = pl+((pr-pl)>>1),res = inf;push_down(p);if(l <= mid){res = min(res,query(ls(p),pl,mid,l,r));}if(mid < r){res = min(res,query(rs(p),mid+1,pr,l,r));}return res;
}
int main(){scanf("%lld",&n);for(long long i = 1; i <= n; i++){scanf("%lld",&num[i]);}build(1,1,n);for(long long i = 1; i <= n; i++){static long long l,r,mid,tp;l = 1,r = n;while(l <= r){mid = l+((r-l)>>1);if(query(1,1,n,mid,n) == 0){tp = mid;l = mid+1;}else{r = mid-1;}}ans[tp] = i;modify(1,1,n,tp,tp,-inf);modify(1,1,n,tp+1,n,i);}for(long long i = 1; i <= n; i++){printf("%lld ",ans[i]);}return 0;
}