A题 裁纸刀
题面
做法
先横着剪,再竖着剪,发现可以在 n * m + 3 次操作后完成裁剪,直觉上感觉就是最少的,至于是不是最少的不知道怎么证明。
于是赛场上直接交了个 443。
B题 灭鼠先锋
题面
做法
推导了一会儿得出 LLLV 的结果,不放心又打了个表验证,发现果然是 LLLV。
代码
#include<bits/stdc++.h>
using namespace std;
int a[2][4];
bool dfs() {
bool all1 = true;
for(int i = 0; i < 2; i++) {
for(int j = 0; j < 4; j++) {
if(!a[i][j]) all1 = false;
}
}
if(all1) return true;
bool res = false;
for(int i = 0; i < 2; i++) {
for(int j = 0; j < 4; j++) {
if(!a[i][j]) {
a[i][j] = 1;
if(!dfs()) res = true;
if(j < 4 && !a[i][j + 1]) {
a[i][j + 1] = 1;
if(!dfs()) res = true;
a[i][j + 1] = 0;
}
a[i][j] = 0;
}
}
}
return res;
}
int main() {
a[0][0] = 1;
cout << dfs();
a[0][0] = 0;
a[0][1] = 1;
cout << dfs();
a[0][1] = 0;
a[0][0] = a[0][1] = 1;
cout << dfs();
a[0][0] = a[0][1] = 0;
a[0][1] = a[0][2] = 1;
cout << dfs();
a[0][1] = a[0][2] = 0;
}
C题 求和
题面
做法
题目要我们求这个式子 ,可以写成 的形式,对于后半部分,做一个前缀和就可以在 O(1) 的时限内完成计算,因此对于整个式子就可以在 O(n) 的时限内完成计算。
具体看代码。
代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
ll n, res = 0, tmp = 0;
cin >> n;
vector<ll> a(n);
for(int i = 0; i < n; i++) {
cin >> a[i];
res += a[i] * tmp;
tmp += a[i];
}
cout << res;
return 0;
}
D题 选数异或
题面
做法
维护 f[i] 表示第 i 个位置左边第一个 A[j] 的值为 A[i] ^ x 的下标 j,这样有一个性质是 a[i] ^ a[f[i]] = x。
比如样例中,i = 3 处, 3 ^ 1 = 2,而 2 这个数字在 3 位置左边第一次出现是在 2 位置。因此 f[3] = 2。
这样,对于每个询问 [l, r],若满足 ,则该区间内存在两个数的异或为 x。
如何求解 f[i]?从左到右扫一遍,用 pos[x] 记录数字 x 当前最右出现的位置。则对于 A[i],f[i] = pos[A[i] ^ x],同时更新 pos[A[i]] = i。对于 A[i] ^ x 不存在的情况,令 f[i] = 0 即可。
还有需要解决的问题是快速求解 的值,我们需要一个支持区间 max 的数据结构,可以使用线段树,ST表......等各种方式维护。
代码中我使用了线段树,预处理时间复杂度 O(nlogn),单次询问 O(logn),总的时间复杂度为 O((n + q) logn)。
代码
#include<bits/stdc++.h>
using namespace std;
const int N = 100000, M = 20;
int st[(N + 1) << 2], pos[1 << M], Left[N + 1];
void pull(int i) {
st[i] = max(st[i << 1], st[i << 1|1]);
}
void build(int i, int tl, int tr) {
if(tl == tr) {
st[i] = Left[tl];
return;
}
int mid = (tl + tr) >> 1;
build(i << 1, tl, mid);
build(i << 1 | 1, mid + 1, tr);
pull(i);
}
int qry(int i, int tl, int tr, int l, int r) {
if(tl >= l && tr <= r) {
return st[i];
}
int mid = (tl + tr) >> 1;
if(mid >= r) {
return qry(i << 1, tl, mid, l, r);
} else if(mid + 1 <= l) {
return qry(i << 1 | 1, mid + 1, tr, l, r);
} else {
return max(qry(i << 1, tl, mid, l, r), qry(i << 1 | 1, mid + 1, tr, l, r));
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, q, x;
cin >> n >> q >> x;
for(int i = 1; i <= n; i++) {
int v;
cin >> v;
Left[i] = pos[v ^ x];
pos[v] = i;
}
build(1, 1, n);
for(int i = 1; i <= q; i++) {
int l, r;
cin >> l >> r;
if(qry(1, 1, n, l, r) >= l) cout << "yes\n";
else cout << "no\n";
}
return 0;
}
E题 爬树的甲壳虫
题面
做法
这题我的做法非常笨,已经知道了有更简洁的做法。
令 f[i] 表示从 i 出发到达 n 的期望次数,则容易得到。
维护 a[i - 1],b[i - 1],含义是
这样,把第二个式子中的 f[0] 代入第一个式子。可以解得 f[i - 1] 关于 f[i] 的表达式
可以写成以下形式:
进而有
由于 且,因此 就是最终的答案。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll P = 998244353;
ll qpow(ll a, ll n) {
ll res = 1;
while(n) {
if(n & 1) res = res * a % P;
a = a * a % P;
n >>= 1;
}
return res;
}
ll inv(ll x) {
return qpow(x, P - 2);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n;
cin >> n;
vector<ll> p(n + 1);
for(int i = 1; i <= n; i++) {
ll x, y;
cin >> x >> y;
p[i] = x * inv(y) % P;
}
vector<ll> a(n + 1), b(n + 1);
a[0] = 1, b[0] = 0;
for(int i = 1; i <= n; i++) {
ll c = (1 - p[i]) * inv((1 - p[i] * a[i - 1]) % P) % P;
ll d = (1 + p[i] * b[i - 1]) % P * inv((1 - p[i] * a[i - 1]) % P) % P;
a[i] = (a[i - 1] * c % P + P) % P;
b[i] = ((a[i - 1] * d + b[i - 1]) % P + P) % P;
}
cout << b[n] << '\n';
return 0;
}
F题 青蛙过河
题面
做法
先思考题意,考虑过河的一条路径,发现如果一条路径能过河,那么同样也可以从这条路径从河对岸过来。因此转化题意为:找到 2x 条过河路径。
什么是过河路径呢?相当于是 {p1, p2, p3, p4...pk} 这样一个位置序列,任意相邻的 pi 距离小于等小青蛙的跳跃能力。每次选择这样的一条路径,相应位置上的 h[i] 减去1。对于一个跳跃能力 k,如果能选出 2x 条路径,那么就说明有解。
使用二分答案的方法。很显然这里 k 越大越有能力“能完成题目所要求的事情”,因此二分是正确的,对小青蛙的跳跃能力 k 二分。
对于一个小青蛙的跳跃能力 k,如何判定呢?这个东西可以看成一个网络流模型(但我们并不采用【流】的算法去求解它),0 位置是源点,n 位置是汇点,询问最大流量。
由于它是一个 DAG(有向无环图)我们可以使用贪心算法去模拟这样一个流的过程(想一想在一个 DAG 上跑最大流会是什么样的?)
我也说不清我是怎么实现的,直接看代码吧。(也许我的实现是错的吧)
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll INF = (ll)2e9;
int n;
ll x;
vector<ll> H;
bool check(int jump) {
ll cnt = 0;
vector<ll> h(n + 1);
for(int i = 0; i < n; i++) h[i] = H[i];
h[n] = h[0];
queue<pair<ll, int>> Q;
Q.push(make_pair(h[0], 0));
for(int i = 1; i <= n; i++) {
while(!Q.empty() && i - Q.front().second > jump) Q.pop();
int now = 0;
while(!Q.empty() && now < h[i]) {
int v = min(Q.front().first, h[i] - now);
now += v;
Q.front().first -= v;
if(Q.front().first == 0) Q.pop();
}
Q.push(make_pair(now, i));
if(i == n) cnt = now;
}
return cnt >= 2 * x;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n >> x;
H.resize(n);
H[0] = INF;
for(int i = 1; i < n; i++) {
cin >> H[i];
}
int l = 1, r = n;
while(l < r) {
int mid = (l + r) / 2;
if(check(mid)) r = mid;
else l = mid + 1;
}
cout << l << '\n';
return 0;
}
G题 最长不下降子序列
题面
做法
考虑如果我们把一段区间修改成一个相同的数字,会修改成什么样的数字?
对于每个 i,我们把它右边 [i + 1, i + k] 范围内全部修改成 a[i],这样一定比把这段区间 [i + 1, i + k]修改成其他数字更优(严格来说应该是不劣)。
那么修改后的最长非降子序列长度 = 以 i 为结尾的最长非降子序列长度 + 在 i + k 位置,暂时把 a[i + k] 改成 a[i] 后,以 i + k 位置为起点的最长非降子序列长度 + (k - 1)。我们把每个位置 i 的结果算出来,取 max 即可。
我们称前者为 f[i], 后者为 g[i][j](g[i + k][a[i]])。二者都可以用线段树维护,具体看代码(我说不清楚我是怎么做的)。
代码
#include<bits/stdc++.h>
using namespace std;
const int N = (int)1e5;
int st[(N + 5) << 2];
void init(int i, int tl, int tr) {
st[i] = 0;
if(tl == tr) return;
int mid = (tl + tr) >> 1;
init(i << 1, tl, mid);
init(i << 1 | 1, mid + 1, tr);
}
void pull(int i) {
st[i] = max(st[i << 1], st[i << 1 | 1]);
}
void upd(int i, int tl, int tr, int p, int v) {
if(tl == tr) {
st[i] = max(st[i], v);
return;
}
int mid = (tl + tr) >> 1;
if(mid >= p) upd(i << 1, tl, mid, p, v);
else upd(i << 1 | 1, mid + 1, tr, p, v);
pull(i);
}
int qry(int i, int tl, int tr, int l, int r) {
if(tl >= l && tr <= r) {
return st[i];
}
int mid = (tl + tr) >> 1;
if(mid >= r) {
return qry(i << 1, tl, mid, l, r);
} else if(mid + 1 <= l) {
return qry(i << 1 | 1, mid + 1, tr, l, r);
} else {
return max(qry(i << 1, tl, mid, l, r), qry(i << 1 | 1, mid + 1, tr, l, r));
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, k;
cin >> n >> k;
vector<int> a(n), b(n), f(n), invf(n);
map<pair<int, int>, int> g;
vector<vector<int>> gv(n);
for(int i = 0; i < n; i++) {
cin >> a[i];
b[i] = a[i];
}
sort(b.begin(), b.end());
b.erase(unique(b.begin(), b.end()), b.end());
for(int i = 0; i < n; i++) {
a[i] = lower_bound(b.begin(), b.end(), a[i]) - b.begin();
}
init(1, 0, n);
for(int i = 0; i < n; i++) {
f[i] = qry(1, 0, n, 0, a[i]) + 1;
upd(1, 0, n, a[i], f[i]);
int r = min(n - 1, i + k);
gv[r].push_back(a[i]);
}
init(1, 0, n);
for(int i = n - 1; i >= 0; i--) {
for(auto v : gv[i]) {
g[make_pair(i, v)] = qry(1, 0, n, v, n) + 1;
}
invf[i] = qry(1, 0, n, a[i], n) + 1;
upd(1, 0, n, a[i], invf[i]);
}
int ans = 0;
for(int i = 0; i < n; i++) {
int r = min(n - 1, i + k);
ans = max(ans, f[i] + g[make_pair(r, a[i])] + r - i - 1);
}
cout << ans << '\n';
return 0;
}
H题 扫描游戏
题面
做法
对每个物品极角排序,棒的长度足够碰到的物品丢进 set ,然后使用 lower_bound 找到离棒最近的一个元素。
这题就是需要让我们模拟一下这个过程,但是我赛场上看错题了,看成了棒逆时针旋转,得知是顺时针旋转后,非常痛苦。
因为不太会计算几何,我的代码写得非常混乱非常丑陋,感觉把方向改一改是对的,不过我也不确定有没有其他 bug。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
inline int to(ll x, ll y) {
if(y > 0 || (x > 0 && y == 0)) return 0;
else return 1;
}
struct Point {
ll x, y, z, id;
bool operator<(const Point& P2) const {
if(x * P2.y - P2.x * y == 0) return id < P2.id;
else return x * P2.y - P2.x * y > 0;
}
bool operator==(const Point& P2) const {
return x * P2.y - P2.x * y == 0 && id == P2.id;
}
};
bool cmp(Point A, Point B) {
return A.x * A.x + A.y * A.y > B.x * B.x + B.y * B.y;
}
int cross(Point A, Point B) {
return A.x * B.y - B.x * A.y;
}
bool sameRank(Point A, Point B) {
return to(A.x, A.y) == to(B.x, B.y) && cross(A, B) == 0;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, rank = 0, add = 0;
ll L;
cin >> n >> L;
ll nowx = 0, nowy = L;
vector<int> res(n, -1);
vector<Point> p[2], solved;
set<Point> s[2];
for(int i = 0; i < n; i++) {
Point pnt;
cin >> pnt.x >> pnt.y >> pnt.z;
pnt.id = i;
if(pnt.x == 0 && pnt.y == 0) {
if(rank == 0) rank = 1, add = 0;
else add += 1;
res[i] = rank;
L += pnt.z;
// !!!
pnt.y = L;
solved.push_back(pnt);
} else {
p[to(pnt.x, pnt.y)].push_back(pnt);
}
}
rank += add;
add = 0;
sort(p[0].begin(), p[0].end(), cmp);
sort(p[1].begin(), p[1].end(), cmp);
bool tag;
do {
tag = false;
while(!p[0].empty()) {
if(L * L >= p[0].back().x * p[0].back().x + p[0].back().y * p[0].back().y) {
s[0].insert(p[0].back());
p[0].pop_back();
} else {
break;
}
}
while(!p[1].empty()) {
if(L * L >= p[1].back().x * p[1].back().x + p[1].back().y * p[1].back().y) {
s[1].insert(p[1].back());
p[1].pop_back();
} else {
break;
}
}
int nowP = to(nowx, nowy);
Point tmp;
tmp.x = nowx, tmp.y = nowy, tmp.z = -1, tmp.id = -1;
auto nxt = s[nowP].lower_bound(tmp);
if(nxt != s[nowP].end()) {
if(solved.empty() || !sameRank(*nxt, solved.back())) {
rank += add + 1;
add = 0;
} else {
add += 1;
}
solved.push_back(*nxt);
res[nxt->id] = rank;
L += nxt->z;
nowx = nxt->x, nowy = nxt->y;
s[nowP].erase(nxt);
tag = true;
} else {
if(nowP == 0) {
tmp.x = -1, tmp.y = 0;
} else {
tmp.x = 1, tmp.y = 0;
}
nowP ^= 1;
auto qwq = s[nowP].lower_bound(tmp);
if(qwq != s[nowP].end()) {
if(solved.empty() || !sameRank(*qwq, solved.back())) {
rank += add + 1;
add = 0;
} else {
add += 1;
}
solved.push_back(*qwq);
res[qwq->id] = rank;
L += qwq->z;
nowx = qwq->x, nowy = qwq->y;
s[nowP].erase(qwq);
tag = true;
} else {
if(nowP == 0) {
tmp.x = -1, tmp.y = 0;
} else {
tmp.x = 1, tmp.y = 0;
}
nowP ^= 1;
auto tvt = s[nowP].lower_bound(tmp);
if(tvt != s[nowP].end()) {
if(solved.empty() || !sameRank(*tvt, solved.back())) {
rank += add + 1;
add = 0;
} else {
add += 1;
}
solved.push_back(*tvt);
res[tvt->id] = rank;
L += tvt->z;
nowx = tvt->x, nowy = tvt->y;
s[nowP].erase(tvt);
tag = true;
}
}
}
}while(tag);
for(int i = 0; i < n; i++) {
cout << res[i] << ' ';
}
return 0;
}
I题 数的拆分
题面
做法
思考后发现,取 y1 = 2,y2 = 3,足够表示所有情况了。问题转变为 n 能否被表示为一个平方数和一个立方数的乘积。
对于单个数字。枚举 1e18 以内所有的立方数(只有 1e6 个),如果它(称为 p)能够整除 n ,且n/p 是一个平方数,那么答案是 yes。
这样我们能够在 1e6 规模的运算次数后算出一个 n 的答案,进一步考虑优化。若 n 能够被表示为一个平方数和一个立方数的乘积,则这个立方数和平方数,必然有一个小于 。也就是说枚举立方数只需要枚举到 1e3,枚举平方数需要枚举到 4e4 这样子就足够了。
然后我就不会做了,我只做了 30% 的部分分数。
// 历史版本(错误的说法,使用Pollard-Rho算法的时间复杂度是不对的):
据说使用一些质因数分解算法可以解决这道题。
// 历史版本
// 学完正解后的版本:
历史版本假了,在瞎说。
正解在这个回答下有人已经说清楚了。
// 学完正解后的版本
部分分代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
bool check2(ll x) {
ll v = sqrt(x);
if((v + 1) * (v + 1) <= x) v += 1;
if(v * v > x) v -= 1;
return v * v == x;
}
const ll N = (ll)1e6;
ll num[N + 5], sq[N + 5];
bool check3(ll x) {
ll v = lower_bound(num + 1, num + N + 1, x) - num;
return num[v] == x;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
for(ll i = 1; i <= N; i++) {
num[i] = i * i * i;
sq[i] = i * i;
}
int T;
cin >> T;
while(T--) {
ll n;
cin >> n;
bool tag = false;
ll z = sqrt(n) + 1;
for(int i = 1; num[i] <= z; i++) {
if(n % num[i] == 0 && check2(n / num[i])) {
tag = true;
break;
}
}
if(!tag) {
for(int i = 1; sq[i] <= z; i++) {
if(n % sq[i] == 0 && check3(n / sq[i])) {
tag = true;
break;
}
}
}
if(tag) cout << "yes\n";
else cout << "no\n";
}
return 0;
}
J题 推导部分和
题面
分析
不会做,听说有一些和这个很像的题,不过赛场上没时间做了,也没有思考,也没有做过原题,有空去补一补吧...
更新做法
在 atcoder 上有一道做法与本题一样的题目。
令前缀和数组 sum[i] 表示 a[1] + a[2] + ... + a[i] 的值。对于一个 [l, r, s],相当于给出了 sum[r] - sum[l - 1] = s 的信息。建一个森林,每个 [l, r ,s] 相当于在点 l - 1 和点 r 之间连了一条表示 sum[r] - sum[l - 1] = s 的边(代码实现上连两条值互为相反数的有向边更为合适)。
对于询问 [l, r],有解当且仅当 r 与 l - 1 同一棵树上时。对于每一棵树,任意指定一个根节点,假设它(root)的值(sum[root])为 0,从它开始 dfs 求出每个点(v)的值(sum[v]),这个过程通过预处理来实现。对于是否在同一棵树上,可以用给每个节点染色的方式实现。
之后的每次询问,只要判断它们是否颜色相同,若相同则答案为 sum[r] - sum[l - 1],否则无解。
今天就到这里了哈~
后续我还会发布更多的学习内容,希望大家可以持续关注,有什么问题可以给我留言或加入群基地一起讨论。
不管你是转行也好,初学也罢,进阶也可,如果你想学编程~
【值得关注】我的【C/C++源码资料学习群】点击进入