Codeforces 题解集合

大概只会放 *2000 以上的题目上去了

[Change Log]

created on 2022.5.6
latest updated on 2022.12.6

目录

Div1

先留着(


Div1 + Div2 & Global Round

Global Round 20

D. Cyclic Rotation ( 1700 )

构造,双指针, multiset,逆向,等价变换

题意

给定长度为 n 的数组 a,和 a 的一个排列数组 b,对 a 进行操作:

  • 如果 al=ar,则 [al,...,ar]=[al+1,...,ar,al],1l<rn

询问能否对 a 进行任意次操作得到 b

数据范围
1n2105
1ai,bin

思路

  • 对操作进行等价变换,每次对 al,ar 的操作相当于把 al 删除, 在 ar 前拷贝一份 ar
  • 尝试逆向,我们看能否从 b 得到 a
  • 双指针构造方案:
    • i,j 指向 a,b 最后一个元素,维护一个 multiset
    • while (b[j] == b[j - 1]) j--; ,向 multiset 中插入 bj
    • 如果 ai=bj , i--,j--
    • 否则,在 multiset 中查找 ai ,找到则 i-- 否则无解
  • 因为每次操作,ar 的位置其实相对是不变的,然后根据第一点,我们从右往前查 multiset中没有的元素一定不合法。

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; typedef double db; #define arr(x) (x).begin(),(x).end() #define x first #define y second #define pb push_back #define endl "\n" using namespace std; /*----------------------------------------------------------------------------------------------------*/ template<class T> struct BIT { int n; vector<T> B; BIT(){}; BIT(int _n) : n(_n), B(_n + 1, 0) {} void init(int _n) { n = _n; B.resize(_n + 1); } inline int lowbit(int x) { return x & (-x); } void add(int x, T v) { for (int i = x; i <= n; i += lowbit(i)) B[i] += v; } T ask(int x) { T res = 0; for (int i = x; i; i -= lowbit(i)) res += B[i]; return res; } }; int main() { ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int T; cin >> T; while (T--) { int n; cin >> n; vector<int> a(n), b(n); for (auto &x : a) cin >> x; for (auto &x : b) cin >> x; int j = n - 1, i = n - 1; multiset<int> s; bool fl = true; while (i && j && fl) { while (i && j && b[j] == b[j - 1]) { s.insert(b[j]); j--; } if (a[i] == b[j]) i--, j--; else { if (s.find(a[i]) != s.end()) { s.erase(s.find(a[i])); i--; } else fl = false; } } fl ? cout << "YES\n" : cout << "NO\n"; } return 0; }

Global Round 21

D. Permutation Graph ( 1900 )

构造,分治,或数据结构贪心

题意

给定长度为 n 的排列,有 n 个点,两点 l, r 之间当且仅当下标区间在 [l,r]a[l],a[r] 为区间的两个最值,可以连边,询问从 1n 的最短路。

数据范围
1n2.5105

思路

  • 抓住题目性质,先找到区间最大值的位置 pos ,假设 pos1,n
  • 横跨 pos 的区间两点是不可能连边的,所以分治求解 dist(1,pos),dist(pos,n)
  • 对于 dist(1,pos) ,我们找出 [1,pos] 区间的最小值位置 mi,递归求解 dist(1,mi),dist(mi,pos), 且 dist(mi,pos) 返回 1。
  • 对于 dist(pos,n) 求解方式相同,依次递归出口为两端点相同。区间满足条件 l != 1 && r != n 就表明是区间两最值端点应当 return 1
  • 然后需要一点细节,就通过本题,时间复杂度为 O(n)

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; typedef double db; #define arr(x) (x).begin(),(x).end() #define x first #define y second #define pb push_back #define endl "\n" using namespace std; /*----------------------------------------------------------------------------------------------------*/ int n; const int N = 3e5; int a[N], premx[N], premin[N], sufmx[N], sufmin[N]; int dist(int l, int r) { // cout << l << " " << r << endl; if (l == r) return 0; if (l == 1) { if (a[r] == premin[r]) { if (a[l] == premx[r]) return 1; for (int i = r - 1; i; i--) if (a[i] == premx[i]) return dist(1, i) + dist(i, r); } else if (a[r] == premx[r]) { if (a[l] == premin[r]) return 1; for (int i = r - 1; i; i--) if (a[i] == premin[i]) return dist(1, i) + dist(i, r); exit(0); } } else if (r == n) { if (a[l] == sufmin[l]) { if (a[r] == sufmx[l]) return 1; for (int i = l + 1; i <= n; i++) if (a[i] == sufmx[i]) return dist(l, i) + dist(i, n); } else if (a[l] == sufmx[l]) { if (a[r] == sufmin[l]) return 1; for (int i = l + 1; i <= n; i++) { if (a[i] == sufmin[i]) return dist(l, i) + dist(i, n); } } } return 1; } int main() { ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int T; cin >> T; while (T--) { cin >> n; for (int i = 1; i <= n; i++){ cin >> a[i]; premx[i] = premin[i] = sufmx[i] = sufmin[i] = 0; } for (int i = 1; i <= n; i++) { if (i == 1) premx[i] = premin[i] = a[i]; else { premx[i] = max(premx[i - 1], a[i]); premin[i] = min(premin[i - 1], a[i]); } } for (int i = n; i; i--) { if (i == n) sufmx[i] = sufmin[i] = a[i]; else { sufmx[i] = max(sufmx[i + 1], a[i]); sufmin[i] = min(sufmin[i + 1], a[i]); } } if (n != 1 && ((premin[n] == a[1] && premx[n] == a[n]) || (premin[n] == a[n] && premx[n] == a[1]))) { cout << 1 << endl; continue; } int pos = max_element(a + 1, a + 1 + n) - a; if (pos == n || pos == 1) { int mi = min_element(a + 1, a + 1 + n) - a; if (pos == n) cout << dist(1, mi) + (n != 1) << endl; else cout << dist(mi, n) + (n != 1) << endl; } else cout << dist(1, pos) + dist(pos, n) << endl; } return 0; }

Div2

Round #264 div2

D. Bear and Floodlight (2200)

状压DP,简单计算几何

题意

在二维坐标上,一个人从 (l,0) 走到 (r,0) ,有 n 盏路灯,每盏灯的照射角度最大为 ai,坐标为 (xi,yi)。询问这个人最多能走多远,到 r 停止。

数据范围
1n20
105lr105
1000xi1000,1yi1000,1ai90

思路

  • 观察数据范围n20 ,两种想法,选和不选和二进制枚举,自然是二进制枚举。
  • 涉及二进制枚举,一个很自然的想法是状压DP,设计一下状态。
  • 发现对于每盏灯的贡献和当前所在点有关。不妨设状态 f[i] 表示状态为 i 时能到达的最远距离。
  • 状态转移:f[i | (1<<j)] = max(f[i|(1<<j)], calc(f[i], j)) ,calc 表示当前在 f[i] 点,第 j 盏灯能到达的最远距离
  • 计算 calc() 函数
    • 可以用 atan2 求出 P(f[i],0) 对应的角,用内角和算出 B(xb,0) 对应的角,然后用其补角 tan 求出 xxb,进而求出 xb
    • 也可以用旋转矩阵,先求出向量 (dx,dy)=(f[i]x,y) 做逆时针旋转矩阵新的向量 (dx,dy)=(dxcos(a)dysin(a),dxsin(a)+dycos(a)) ,那么 xb=xydx/dy
    • 两种情况都需要特判,最远点是无穷大或者其他边界问题。
  • 目标答案为 f[(1<<n)1] ,时间复杂度为 O(n2n)

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; typedef double db; #define arr(x) (x).begin(),(x).end() #define x first #define y second #define pb push_back #define endl "\n" using namespace std; /*----------------------------------------------------------------------------------------------------*/ const double PI = acos(-1); const int N = 1 << 20; int n; double l, r; double f[N]; struct P{ double x, y, a; }; // double cal(double l, P& p) { // auto [x, y, a] = p; // double dx = l - x, dy = -y; // double nx = dx * cos(a) - dy * sin(a), ny = dx * sin(a) + dy * cos(a); // if (fabs(ny) < 1e-12) return r; // if (ny > 0) return r; // return min(x - y * nx / ny, r); // } double cal(double l, P& p) { auto [x, y, a] = p; double angleA = atan2(y, x - l); if (angleA + a >= PI) return r; double angleB = PI - angleA - a; if (angleB == PI / 2) return x; return x + y / tan(angleB); } int main() { cin >> n >> l >> r; vector<P> p(n); r -= l; for (int i = 0; i < n; i++) { cin >> p[i].x >> p[i].y >> p[i].a; p[i].x -= l; p[i].a *= PI / 180; } for (int i = 0; i < 1 << n; i++) { for (int j = 0; j < n; j++) { if ((i >> j & 1) == 0) { int nxt = i + (1 << j); f[nxt] = max(f[nxt], cal(f[i], p[j])); } } } double ans = f[(1<<n) - 1]; cout << fixed << setprecision(9) << min(ans, r) << endl; return 0; }

E. Bear in the Field (2300)

矩阵快速幂

题意

在大小为 n×n 的棋盘上,一个点起始坐标为 (sx,sy) 起始速度是 (dx,dy) ,询问经过 t 秒后的位置,对于每一秒:

  • 点处于 (i,j),给速度的贡献为 k, 速度将变为 (dx+k,dy+k)
  • 移动到 (x+dx1,y+dy1), 这里的 dx,dy 已经更新
  • 每个点给速度的贡献 +1

数据范围
1n109
1sx,syn
100dx,dy100
0t1018

思路

  • 观察数据范围,t1018 很大很大,相当于询问你经过 t 步之后的位置,显然的矩阵快速幂。
  • 状态矩阵参数:(dx,dy,x,y,0,1) 其中 0 其实代表当前经过的时间。
  • 转移矩阵,先看如何转移:
    • 因为运算过程中涉及对 n 取模,先将坐标下标从 0 开始比较好算。
    • dx=dx+(x+y+2)+t
    • dy=dy+(x+y+2)+t
    • x=x+dx+(x+y+2)+t
    • y=y+dy+(x+y+2)+t
    • t=t+1
    • 1=1
    • 故状态矩阵为

    [101000010100112100111200111110222211]

  • 时间复杂度为 O(63logt)

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; typedef double db; #define arr(x) (x).begin(),(x).end() #define x first #define y second #define pb push_back #define endl "\n" using namespace std; /*----------------------------------------------------------------------------------------------------*/ ll n, val; #define _DEBUG template <typename T, const int N> struct Mat { int len; Mat() { memset(data, 0, sizeof(data)); len = N; } T *operator[](int i) { return data[i]; } const T *operator[](int i) const { return data[i]; } T add(T a, T b){ return (a + b + val) % n; } Mat &operator += (const Mat &o) { for (int i = 0; i < len; ++i) for (int j = 0; j < len; ++j) data[i][j] = add(data[i][j], o[i][j]); return *this; } Mat operator + (const Mat &o) const { return Mat(*this) += o; } Mat &operator -= (const Mat &o) { for (int i = 0; i < len; ++i) for (int j = 0; j < len; ++j) data[i][j] = add(data[i][j], -o[i][j]); return *this; } Mat operator-(const Mat &o) const { return Mat(*this) -= o; } Mat operator*(const Mat &o) const { static T buffer[N]; Mat result; for (int j = 0; j < len; ++j) { for (int i = 0; i < len; ++i) buffer[i] = o[i][j]; for (int i = 0; i < len; ++i) for (int k = 0; k < len; ++k) result[i][j] = (result[i][j] + (data[i][k] * buffer[k]) + val)% n; } return result; } Mat power(ull k) const { Mat res; for (int i = 0; i < len; ++i) res[i][i] = T{1}; Mat a = *this; while (k) { if (k & 1ll) res = res * a; a = a * a; k >>= 1ll; } return res; } private: T data[N][N]; }; int main() { ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); ll sx, sy, dx, dy, t; cin >> n >> sx >> sy >> dx >> dy >> t; sx--, sy--; val = 10000 * n; Mat<ll, 6> a; Mat<ll, 6> b; b[0][0] = dx, b[0][1] = dy, b[0][2] = sx, b[0][3] = sy, b[0][4] = 0, b[0][5] = 1; a[0][0] = a[0][2] = a[1][1] = a[1][3] = a[2][0] = a[2][1] = a[2][3] = 1; a[3][0] = a[3][1] = a[3][2] = a[4][0] = a[4][1] = a[4][2] = a[4][3] = a[4][4] = a[5][4] = a[5][5] = 1; a[2][2] = a[3][3] = a[5][0] = a[5][1] = a[5][2] = a[5][3] = 2; #ifdef _DEBUG for (int i = 0; i < 6; i++) { for (int j = 0; j < 6; j++) { cout << a[i][j] << " "; } cout << endl; } cout << endl; for (int i = 0; i < 6; i++) cout << b[0][i] << " "; cout << endl; #endif a = a.power(t); Mat<ll, 6> res; b = b * a; cout << b[0][2] + 1 << " " << b[0][3] + 1 << endl; return 0; }

Round #564 div2

D.Nauuo and Circle ( 1900 ) (结论、计数)

题意

给了一颗 n 个节点的树,询问存在多少种合法排列,使得 n 个点在圆环上排列对应位置上,相连的边不会有相交。

答案模 998244353

数据范围
2n2105

思路

  • 核心点:一个结论:一个节点的子树所有点在圆环上连续的一段。
  • 所以一个节点的子节点可以有阶乘级别的排列。
  • 更正式的表达,f[u]=((u!=root)+son[u]!)Πvson[u]f[v]
  • 由于整棵树根可以随意设置,但最后效果其实和 1 放在 n 个位置上的效果一样, 所以最后答案等于 nf[1] , 而实际上把每个点的度数阶乘相乘即可。

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; typedef double db; #define arr(x) (x).begin(),(x).end() #define x first #define y second #define pb push_back #define endl "\n" using namespace std; /*----------------------------------------------------------------------------------------------------*/ const int N = 2e5 + 10, mod = 998244353; ll fact[N], d[N]; // f[u] = ((u != root) + son[u]!) * \PI{f[v]} int main() { ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int n; cin >> n; for (int i = 1; i < n; i++) { int a, b; cin >> a >> b; d[a] ++, d[b]++; } fact[0] = 1; for (int i = 1; i <= n; i++) fact[i] = fact[i - 1] * i % mod; ll ans = n; for (int i = 1; i <= n; i++) ans = ans * fact[d[i]] % mod; cout << ans << endl; return 0; }

Round #747 div2

C. Make Them Equal ( 1200 )

题意

输入长度为 n 的字符串 s 和一个字符 c ,可以进行一个操作,对于 1xn ,可以将不能整除 xi(1in) ,所在的下标字符替换成 c ,问最少经过多少次操作可以使 s 每个字符都是 c

思路

  • 由整除出发,
    • 对于 x=n 而言,1in1 都可以被替换。
    • 对于 x=n1 而言,i=n 可以被替换。
  • 因此最多需要2次操作就可以将 s 替换成想要的字符串。
  • 对于做 0 次操作,遍历数组就行。
  • 对于做 1 次操作,当数组中i<nii 的倍数下标位置的字符都是 c 的话,只需要一次操作就可以把 s 都替换成 c ,输出 i 的下标。
  • 对于做 2 次操作,就是 i<nii 的倍数下标位置的字符都是 c ,输出 n1n

赛中

上述的第一点想到了,但是忽略了做1次操作的做法,也是这道题的核心,由第1点衍生出,i可以替换不是它倍数的下标位置(其实就是题意)。太蠢了

Solution

#include<iostream> #include<cstring> using namespace std; int main(){ int T; scanf("%d", &T); while(T--){ int n; char c; string s; cin >> n >> c; cin >> s; bool flag = true; for(int i = 0; i < n; i++){ if(s[i] != c){ flag = false; break; } } if(flag){ printf("0\n"); continue; } bool f = false; for(int i = 1; i <= n; i++){ flag = true; for(int j = i; j <= n; j += i){ if(s[j - 1] != c){ flag = false; break; } } if(flag){ f = true; printf("1\n%d\n", i); break; } } if(!f){ // 1-n-1不存在能代替所有字符的位置 printf("2\n%d %d\n", n - 1, n); } } return 0; }

Round #782 div2

D. Reverse Sort Sum ( 1900 )

构造、逆序操作

题意

求一个长度为 n 的 01 数组 A,对 A 进行 n 次操作, 每次对 [1,n] 的区间进行排序结果累加到 C 数组中,已知 C 数组求 A 数组。

数据范围
1n2105
0cin

思路

  • 观察1:将 C 数组求和 /n 可以得到 1 的个数 num。由贡献思考
  • 观察2:如果 Cn=n/1 ,那么 An=1/0
  • 由观察1得到 1 的个数,抓住题目性质,在第 n 次操作时,是由后缀为 num1 的数组累加。An 已经求出,看前 n1 位,如果我们把第n 次的后缀累加给撤销,得到的 C[1,n1] 其实是第 n1 次操作累加之后的 C 的前 n1 位,这时候可以不用管 An,Cn 。然后同理如果 Cn1=n1/1,An1=1/0 。就这样往前递推。
  • 但需要注意,如果该位为 1num 需要减 1,很好理解,在 i 之前 Ai=1 没有在排序范围内自然也不属于后缀。
  • 涉及区间更新操作就懒得 O(n) 搞来搞去了,直接上差分树状数组,复杂度 O(nlogn)

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; typedef double db; #define arr(x) (x).begin(),(x).end() #define x first #define y second #define pb push_back #define endl "\n" using namespace std; /*----------------------------------------------------------------------------------------------------*/ template<class T> struct BIT { int n; vector<T> B; BIT(){}; BIT(int _n) : n(_n), B(_n + 1, 0) {} void init(int _n) { n = _n; B.resize(_n + 1); } inline int lowbit(int x) { return x & (-x); } void add(int x, T v) { for (int i = x; i <= n; i += lowbit(i)) B[i] += v; } T ask(int x) { T res = 0; for (int i = x; i; i -= lowbit(i)) res += B[i]; return res; } }; int main() { ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int T; cin >> T; while (T--) { int n; cin >> n; vector<int> c(n + 1), ans(n + 1, 0); BIT<int> bt(n); ll num = 0; for (int i = 1; i <= n; i++) { cin >> c[i]; num += c[i]; bt.add(i, c[i]), bt.add(i + 1, -c[i]); } num /= n; for (int i = n; i; i--) { if (bt.ask(i) >= i) { ans[i] = 1; bt.add(i - num + 1, -1); bt.add(i + 1, 1); num--; } } for (int i = 1; i <= n; i++) cout << ans[i] << " "; cout << endl; } return 0; }

Round #788 div2

E. Hemose on the Tree ( 2200 )

构造

题意

给了 n 个点的树, n2 的幂次,边权和点权是 [1,2n1] 的排列。询问从任意点为根,从根出发途中经过的的点和路径权值 最大值的最小值是多少?

数据范围
1p17
2p=n

思路

  • 首先如果单独选一个大于 2n 的点为根,最大值 2p ,所以考虑根权值为 2p ,并且途径最大值为 2p ,其实就是一个构造
  • 接下来需要 n1 对数分别为 x,x+2p
  • 分类讨论构造一下
    • 父节点 >=2p,子节点值为 x ,边为 x+2p
    • 父节点 <2p,子节点值为 x+2p,边为 x
  • 这样构造到根的子节点,异或和为0,而由于根大(指大于 2p) ,那么子节点小,到了子节点到子节点的边异或值为 x,到了子节点的子节点异或值为 2p ,如此循环往复,所有值一定 2p
#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; #define x first #define y second #define pb push_back #define mkp make_pair #define endl "\n" using namespace std; const int N = 1 << 18; int n, p; vector<PII> edge[N]; int ans_edge[N], ans_node[N], tot; void dfs(int u, int fa){ for(auto v: edge[u]){ if(v.x == fa) continue; if(ans_node[u] & n){ ans_node[v.x] = ++tot; ans_edge[v.y] = tot + n; } else{ ans_edge[v.y] = ++tot; ans_node[v.x] = tot + n; } dfs(v.x, u); } } int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int T; cin >> T; while(T--){ tot = 0; cin >> p; n = 1 << p; for(int i = 1; i < n; i++){ int a, b; cin >> a >> b; edge[a].pb({b, i}); edge[b].pb({a, i}); } ans_node[1] = 1 << p; dfs(1, -1); cout << 1 << endl; for(int i = 1; i <= n; i++) cout << ans_node[i] << " "; cout << endl; for(int i = 1; i <= n - 1; i++) cout << ans_edge[i] << " "; cout << endl; for(int i = 1; i <= n; i++) edge[i].clear(); } return 0; }

Round #789 div2

D. Tokitsukaze and Meeting ( 1700 )

DP + 行列分开计算贡献

题意

nm 矩阵左上角里塞 0 和 1, m 列的元素前往下一行第1列, 依次后移, 询问每一次行和列中有多少个行和列里有 1

数据范围
nm106

思路

  • 行列分开考虑.
  • 对于列, 进入 0 答案不变, 进入 1 如果第一列此时没有元素为1, 列答案++
  • 对于行, 答案显然与 i - m 的行答案有关系, 相当于新加入了一行, 如果这行中有 1, 则行答案++, 具体实现见代码
#include<bits/stdc++.h> #define pb push_back #define endl "\n" using namespace std; int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int T; cin >> T; while(T--){ int n, m; cin >> n >> m; string s; cin >> s; s = " " + s; vector<int> dp(n * m + 1, 0), ans(n * m + 1, 0); int cnt_col = 0, last = -0x3f3f3f3f; for(int i = 1; i <= n * m; i++){ if(s[i] == '1'){ bool fl = false; for(int j = i - m; j > 0 && !fl; j -= m) if(s[j] == '1') fl = true; if(!fl) cnt_col++; last = i; } dp[i] = dp[max(i - m, 0)] + ((i - last < ) ? 1 : 0); ans[i] = dp[i] + cnt_col; } for(int i = 1; i <= n * m; i++) cout << ans[i] << " "; cout << endl; } return 0; }

Round #795 div2

D. Max GEQ Sum (1800)

题意转换, 单调栈, 前缀和, 最值查询

题意

给了一个长度为 n 的数组 a, 询问该数组是否满足任意连续子序列 [i,j] 满足:

max(ai,ai+1,,aj)ai+ai+1++aj

数据范围
1n2105
109ai109

思路

  • 题意转换: 实际询问从 ai 向两边扩展, 在未达到更新最大值的位置 [l,r], 考虑一般情况下, 前缀和 pre[r]pre[l1]>a[i] 是否成立
  • 以上转换, 可以把问题拆分多个问题空间的不重不漏的子集.
  • 显然需要用单调栈\笛卡尔树 求出左右两边大于 ai 的第一个下标分别记为 [L,R]
  • 则第一点的式子转化为是否存在 l,r(L<l<i,i<r<R) 满足 sum[l,i1]+sum[i+1,r]+a[i]>a[i].
  • sum[l,i1]+sum[i+1,r]>0 , 仔细观察发现等式只需要只要其中一个和大于 0 就能满足了
  • 故问题转化为求是否 maxsum[l,i1]>0ormaxsum[i+1,r]>0
  • 实际代码形式: querysuf(L+1, i-1) - suf[i] > 0 || querypre(i + 1, R - 1) - pre[i] > 0 , 时间复杂度为 O(nlogn)

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; typedef double db; #define arr(x) (x).begin(),(x).end() #define x first #define y second #define pb push_back #define mkp make_pair #define endl "\n" using namespace std; const int N = 2e5 + 10, M = 19; const ll INF = -2e18; ll stk[N], L[N], R[N], n, a[N], s[N], suf[N]; ll st1[N][M], st2[N][M]; void init(){ for(int i = 0; i < M; i++) for(int j = 1; j + (1 << i) - 1 <= n; j++) // 区间长度、右端点-1 if(!i) st1[j][i] = s[j]; else st1[j][i] = max(st1[j][i - 1], st1[j + (1 << (i - 1))][i - 1]); for(int i = 0; i < M; i++) for(int j = 1; j + (1 << i) - 1 <= n; j++) // 区间长度、右端点-1 if(!i) st2[j][i] = suf[j]; else st2[j][i] = max(st2[j][i - 1], st2[j + (1 << (i - 1))][i - 1]); } ll querypre(int l, int r){ if(l > r) return INF; int len = r - l + 1; int k = log(len) / log(2); return max(st1[l][k], st1[r - (1 << k) + 1][k]); // 查询处记得+1 } ll querysuf(int l, int r){ if(l > r) return INF; int len = r - l + 1; int k = log(len) / log(2); return max(st2[l][k], st2[r - (1 << k) + 1][k]); // 查询处记得+1 } int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int T; cin >> T; while(T--){ cin >> n; suf[n + 1] = 0; for(int i = 1; i <= n; i++){ cin >> a[i]; s[i] = s[i - 1] + a[i]; } for(int i = n; i; i--) suf[i] = suf[i + 1] + a[i]; init(); int top = 0; stk[0] = 0; for(int i = 1; i <= n; i++){ while(top && a[stk[top]] <= a[i]) top--; L[i] = stk[top]; stk[++top] = i; } top = 0; stk[0] = n + 1; for(int i = n; i >= 1; i--){ while(top && a[stk[top]] <= a[i]) top--; R[i] = stk[top]; stk[++top] = i; } bool fl = true; for(int i = 1; i <= n; i++){ int l = L[i] + 1, r = R[i] - 1; if(querysuf(l, i - 1) - suf[i] > 0 || querypre(i + 1, r) - s[i] > 0){ fl = false; break; } } fl ? cout << "YES\n" : cout << "NO\n"; } return 0; }

Round #801 div2

C. Zero Path ( 1700 )

方格dp、思维

题意

询问在大小为 (n,m) 的方格中,每个格子上值为 11,是否存在一条从 (1,1)(n,m) 的路径,满足路径和为 0

数据范围
1n,m1000

思路

  • 这个题很重视观察性质。
  • 如果 n+m 为偶数,则最后一定经过 奇数 个格子,此时无解。 那么一定经过偶数个格子。
  • 明显可以通过 dp 预处理出到达每个格子的最大值和最小值。如果 dp[n][m][] 包含 0 则有解,否则无解,证明:
    • 如果不包含显然无解。证明包含一定有解。
    • 因为经过偶数个格子,最后的和一定是偶数,(奇数-奇数,偶数-偶数)
    • 如果进行一次调整让和变化,一定是 2,0,2 。因为本来和就是偶数,所以只要区间包含 0, 一定可以调整到和为 0, 证毕。
  • 时间复杂度 O(nm) ,代码上还需要一些细节。一道不错的题。

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; typedef double db; #define arr(x) (x).begin(),(x).end() #define x first #define y second #define pb push_back #define endl "\n" using namespace std; /*----------------------------------------------------------------------------------------------------*/ const int N = 1010; int n, m; int g[N][N], dp[N][N]; int main() { ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int T; cin >> T; while (T--) { cin >> n >> m; auto dp = Vector<int>(2, n + 2, m + 2); auto g = Vector<int>(n + 1, m + 1); for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) cin >> g[i][j]; if ((n + m - 1) & 1) { cout << "NO\n"; continue; } for (int i = 1; i <= m; i++) { dp[0][0][i] = 1e9; dp[1][0][i] = -1e9; } for (int i = 1; i <= n; i++) { dp[0][i][0] = 1e9; dp[1][i][0] = -1e9; } dp[0][1][1] = dp[1][1][1] = g[1][1]; for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { if(i == 1 && j == 1) continue; dp[0][i][j] = g[i][j] + min(dp[0][i - 1][j], dp[0][i][j - 1]); dp[1][i][j] = g[i][j] + max(dp[1][i - 1][j], dp[1][i][j - 1]); } } bool fl = false; // cout << dp[0][n][m] << " " << dp[1][n][m] << endl; if (dp[0][n][m] <= 0 && dp[1][n][m] >= 0) fl = true; fl ? cout << "YES\n" : cout << "NO\n"; } return 0; }

Round #802 div2

C. Helping the Nature ( 1700 )

差分、构造

题意

给了长度为 n 数组 a,可以进行三种操作:

  1. 对任意前缀所有元素 1 .
  2. 对任意后缀所有元素 +1 .
  3. 对全部元素 =1 .

询问最少经过多少次操作可以将所有元素置 0

数据范围
1n200000
1ai109

思路

  • 由于是区间操作,不妨用差分来思考
  • 构造差分数组 b[i] = a[i] - a[i - 1], 目标状态 b[i],in 全为 0
  • 对于三种操作,等价于对差分数组:
    1. b[1]--, b[i>1]++
    2. b[n + 1]++, b[i<=n]--
    3. b[1]++
  • 所以先将 2in 的所有 b[i] 全部设为 0,最后将 b[1]0,将 b[1]0 三种操作都可以,贪心取最划算的即可。
  • 时间复杂度 O(n)

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; typedef double db; #define arr(x) (x).begin(),(x).end() #define x first #define y second #define pb push_back #define endl "\n" using namespace std; /*----------------------------------------------------------------------------------------------------*/ int main() { ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int T; cin >> T; while (T--) { int n; cin >> n; vector<ll> a(n + 2, 0), b(n + 2, 0); ll ans = 0; for (int i = 1; i <= n + 1; i++) { if (i <= n) cin >> a[i]; b[i] = a[i] - a[i - 1]; } for (int i = 2; i <= n; i++) { ans += abs(b[i]); if (b[i] > 0) { b[n + 1] += b[i]; b[i] = 0; } else { b[1] += b[i]; b[i] = 0; } } cout << ans + max(abs(b[1]), abs(b[n + 1])) << endl; } return 0; }

Education Round


Round 42 (Rated for Div. 2)

E. Byteland, Berland and Disputed Cities ( 2200 )

贪心

题意

给了 n(2n2105) 个点,坐标 109x109 。每个点属于 P,B,R 其中一个。

询问在 {P,B} 点集中连接所有点和 {P,R} 点集中连接所有点,两者加起来的花费最小值。

思路

  • 一点点考虑发现是个贪心分类讨论题。
  • 对于没有 P 点存在,答案很显然。
  • 如果两个 P 点中夹杂 B/P。对于这一段连接方式有两种。
    • 相邻两点不管什么颜色直接连接。
    • 先连接这一段的两个端点 P,这样在上一种连接方式下,可以扣除中间跨度最大的相邻点的连线,这样可能会更优。
  • 时间复杂度 O(n)

Solution

CF962E-评测记录


Round 47 (Rated for Div. 2)

这场vp, 打的很烂, D 题读漏条件, B 题贪错方向

B. Minimum Ternary String ( 1400 )

贪心
题意

给定长度为 n(1n105) 的串, 只有 0,1,2 组成, 相邻 01 可以互换, 相邻 12 可以互换, 询问任意互换次数后整个串的最小字典序

思路

  • 主要把握住题目条件, 发现 1 可以与 0 和 2 换, 所以不会有 1 在 2 之后, 第一个 2 之前的 0 在最前面, 之后相对顺序不变

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; typedef double db; #define arr(x) (x).begin(),(x).end() #define x first #define y second #define pb push_back #define mkp make_pair #define endl "\n" using namespace std; int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); string s; cin >> s; int n = s.size(); queue<int> q1, q2; bool fl = false; int pos = 0, cnt1 = 0; for(int i = 0; i < n; i++){ if(s[i] == '2'){ if(!fl) pos = i; fl = true; } else if(s[i] == '1') cnt1++; } if(!fl){ for(int i = 0; i < n - cnt1; i++) cout << 0; for(int i = 0; i < cnt1; i++) cout << 1; } else{ int cnt0 = 0; for(int i = 0; i < pos; i++){ if(s[i] == '0') cnt0++; } for(int i = 0; i < cnt0; i++) cout << 0; for(int i = 0; i < cnt1; i++) cout << 1; for(int i = pos; i < n; i++){ if(s[i] != '1') cout << s[i]; } } return 0; }

C. Annoying Present ( 1700 )

贪心---货仓选址模型利用, 注意开 long double

题意

输入 n,m , 表示长度为 n 的数组, m 次操作, 每次操作有 xd, 每次操作选择一个下标 i, 对数组所有元素加上 x+d|ij|, 询问最终操作后数组平均值最大值

数据范围

1n,m105
103xi,di103

思路

  • 平均值最大 -> 数组和最大, 负数影响最小, 正数影响最大.
  • x 对和的贡献固定, 考虑距离的贡献, 由货仓选址模型可知, 距离和最小的选点应在中点, 反之最大的点应在边缘点, 之后便可求解该题
  • 观察数据范围, 要开 long double

Solution

#include<bits/stdc++.h> typedef long long ll; typedef unsigned long long ull; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef double db; #define arr(x) (x).begin(),(x).end() #define x first #define y second #define pb push_back #define mkp make_pair #define endl "\n" using namespace std; int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int n, m; cin >> n >> m; long double sum = 0; long double tot1 = 0, tot2 = 0; int mid = (n + 1) / 2; for(int i = 1; i <= n; i++){ tot1 += abs(i - mid); tot2 += i - 1; } while(m--){ long double x, d; cin >> x >> d; sum += (long double)n * x; if(d < 0) sum += d * tot1; else sum += tot2 * d; } cout << fixed << setprecision(7) << sum / n << endl; return 0; }

D. Relatively Prime Graph ( 1700 )

构造, 边上两点编号互质, 图要求连通

题意

给定一个图, 询问是否能构造出 n 个点, m 条边的连通图, 任意边两端点编号互质

数据范围
1n,m100000

思路

  • 观察数据范围, 最多只有 m,1000000 条边, 对于每一个数与其互质小于它的数字个数为 Φ(x), 累加到 600 左右时, 已经大于了 105
  • 故可以暴力用 gcd() 判断两点互质来连边
  • 记得是连通图, 即 m>n1

Solution

#include<bits/stdc++.h> typedef long long ll; typedef unsigned long long ull; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef double db; #define arr(x) (x).begin(),(x).end() #define x first #define y second #define pb push_back #define mkp make_pair #define endl "\n" using namespace std; ll gcd(ll a, ll b){ return b ? gcd(b, a % b) : a; } int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int n, m; scanf("%d%d", &n, &m); vector<PII> ans; int now = 1, cnt = 0; if(m < n - 1){ printf("Impossible\n"); return 0; } while(cnt < m && now <= n){ if(now == 1){ for(int i = 2; i <= n && cnt < m; i++){ ans.pb({now, i}); cnt++; } now++; continue; } for(int i = now + 1; i < 2 * now && cnt < m; i++){ if(gcd(i, now) != 1) continue; for(int j = 0; i + j * now <= n && cnt < m; j++){ int v = i + j * now; ans.pb({now, v}); cnt++; } } now++; } if(cnt < m) printf("Impossible\n"); else{ printf("Possible\n"); for(int i = 0; i < m; i ++) printf("%d %d\n", ans[i].x, ans[i].y); } return 0; }

E. Intercity Travelling ( 2000 )

组合数, 递推, 期望

题意

你要从 0 n ,有 n 个站点 (1 n),每个站点都有 1/2 的几率有休息点。你连续坐 k 站时,每两站间的疲劳值为 a1,a2ak
如果第 k 站有休息点,那么你可以在此处休息,然后接下来的站点的疲劳值又从 a1 开始。求 p()2(n1)

数据范围
1n,ai106

思路

  • 我们要求的其实是疲劳值之和, 直接算肯定不好搞, 要按贡献来算, 有两种思路, 考虑每两点之间的答案贡献, 考虑每个 ai 的贡献, 前者请参考官方题解
  • 计算每个 ai 的贡献其实是求每个 ai 的出现次数
  • 考虑 a1 , 由于 0 点的特殊性, 0>1 一定有一个 a1, 无论后面怎么走, 有 2n1 次, 考虑后面的 n 个点相邻两点 x 和 y, 一定是 x 休息, 其他点无所谓(但不考虑终点的休息)有 (n1)2n2
  • 同样考虑 a2, 对于 0 点特殊考虑, 0>1>2, 2 及以后的店怎么搞无所谓, 有 2n2 次, 同样和前面一样, x, y, z 三个点, 一定是 x 休息, y 不休息, 其他点无所谓, 有 (n2)2n3
  • 所以对于任意 ai 出现次数为 2ni+(ni)2ni1

Solution

#include<bits/stdc++.h> typedef long long ll; typedef unsigned long long ull; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef double db; #define arr(x) (x).begin(),(x).end() #define x first #define y second #define pb push_back #define mkp make_pair #define endl "\n" using namespace std; const int mod = 998244353, N = 1e6 + 10; ll mi[N]; int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); mi[0] = 1; int n; cin >> n; for(int i = 1; i <= n; i++) mi[i] = mi[i - 1] * 2 % mod; ll ans = 0; for(int i = 1; i <= n; i++){ ll a; cin >> a; ans = (ans + a * (mi[n - i] + 1ll * (n - i) * mi[n - i - 1] % mod) % mod) % mod; } cout << ans << endl; return 0; }

Round 84 (Rated for Div. 2)

D. Infinite Path ( 2200 )

置换环、数论

题意

定义排列乘法 c=a×b , c[i]=b[a[i]] 。自然有排列的幂次 pk=ppp

给定在长度为 n(1n2×105) 的排列 p 。和每个点的颜色 c 。询问最小的幂次 k 能存在一个
i 使得 i,p[i],p[p[i]],p[p[p[i]]], 都是同一种颜色。

思路

  • 读完题目很容易联想置换环这个东西,每个点 ip[i] 连边,会形成若干个环。
  • 对于每个环的内部,经过环的长度幂次 len 一定能存在一个 i
  • 还有更快的方式就是,经过环的因子次 L 变换以后,会分成 L 个长度为 len/L 的小环。如果存在一个小环所有点颜色相同则 L 合法。
    • 给一个样例 p: 6 5 4 2 1 3 color: 1 1 1 3 4 5 答案为 3
  • 解法就是找出所有的环,再暴力枚举环长度的所有因子,来检验是否合法,简单分析复杂度是够的。

Solution

评测记录


Round 115 (Rated for Div. 2)

这场是vp的,4题138罚时,performance在 1835

E. Staircase ( 2100 )

经典放格计数, DP 预处理, 修改只考虑单点对答案的贡献

题意

一张 nm 的图,有两种楼梯形,L7 形,即向下向右和向右向下这样无限重复,单点和横着的两个点和竖着的两个点也算,有 q 个询问,每次将每个点的权值异或 1 ,如果某个点的值为 0 ,那么这个点就不能用,再询问贡献。

数据范围
1n1000
1m1000
1q104

思路

  • 先处理所有格子都 free 的所有方案数,用 dp 来做
    • dp[i][j][0] 表示该点作为左下角的方案数、dp[i][j][1] 表示该点作为右下角的方案数
    • 所有状态初始化为 1, 状态转移:dp[i][j][0] += dp[i - 1][j][1], dp[i][j][1] += dp[i][j - 1][0]
    • 以每个格子结尾对答案的贡献就是 dp[i][j][0] + dp[i][j][1] - 1
  • 然后再考虑对于每个询问来处理,每次询问给出 (x,y) 点,这里有一个处理技巧:只考虑这个点对答案的贡献
  • 从该点出发(先假定这个格子 free),往上和往下最多能走的合法格子数,相乘-1就是贡献,当然注意有两种走法

自己想法

不会对每个询问贡献单独算,不太会处理所有格子free的方案数

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; #define x first #define y second #define pb push_back #define mkp make_pair #define endl "\n" using namespace std; const int N = 1010; int dp[N][N][2], sta[N][N]; int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int n, m, q; cin >> n >> m >> q; ll ans = 0; for(int i = 1; i <= n; i++){ for(int j = 1; j <= m; j++){ // 0左下角 sta[i][j] = 1; dp[i][j][0] = dp[i][j][1] = 1; dp[i][j][0] += dp[i - 1][j][1]; dp[i][j][1] += dp[i][j - 1][0]; ans += dp[i][j][0] + dp[i][j][1] - 1; } } int dx[4] = {0, -1, 1, 0}, dy[4] = {-1, 0, 0, 1}; // 左、上、下、右 while(q--){ int x, y; cin >> x >> y; int sign = 1, term = 0; if(sta[x][y]) sign = -1; else term = 1, sta[x][y] = 1; int up = 0, down = 0, dir = 0; ll d = 0; int nx = x, ny = y; while(sta[nx][ny]){ up++; nx = nx + dx[dir]; ny = ny + dy[dir]; dir ^= 1; } nx = x, ny = y, dir = 2; while(sta[nx][ny]){ down++; nx = nx + dx[dir]; ny = ny + dy[dir]; dir = 5 - dir; } d += 1ll * up * down; up = down = 0; nx = x, ny = y, dir = 1; while(sta[nx][ny]){ up++; nx = nx + dx[dir]; ny = ny + dy[dir]; dir ^= 1; } nx = x, ny = y, dir = 3; while(sta[nx][ny]){ down++; nx = nx + dx[dir]; ny = ny + dy[dir]; dir = 5 - dir; } d += 1ll * up * down; d -= 1; sta[x][y] = term; ans += d * sign; cout << ans << endl; } return 0; }

Round 116 (Rated for Div. 2)

D. Red-Blue Matrix ( 2400 )

题意

给了一个 nm 的矩阵,每个元素有一个值,现在要把矩阵的行染成蓝色或红色(必须两个颜色都有),染完后将矩阵切成左右两个矩阵,切割行为 k
左边矩阵红色元素都比蓝色元素大,右边矩阵蓝色元素都比红色元素

数据范围
2n,m5×105,nm1e6
aij106

思路

  • 首先无论分割点在哪里,第一列永远要被考虑,如果将行按第一列元素大小排序,那么永远只有前缀行染成蓝色
  • 然后枚举切割点和染色行检验合法性,要预处理顶点出发的子矩阵最值做到O(1)查询

自己想法

不知道第一列元素的重要性,甚至看了题解半天没反应出来怎么预处理子矩阵最值

Solution

#include<bits/stdc++.h> typedef long long ll; typedef unsigned long long ull; #define x first #define y second #define pb push_back #define mkp make_pair #define endl "\n" using namespace std; typedef std::pair<int, int> PII; typedef std::pair<vector<int>, int> PLL; int n, m; bool cmp(PLL& a, PLL& b){ return a.x[1] < b.x[1]; } int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int T; cin >> T; while(T--){ cin >> n >> m; vector<PLL> a(n + 1, {vector<int>(m + 1, 0), 0}); for(int i = 1; i <= n; i++){ a[i].y = i; for(int j = 1; j <= m; j++) cin >> a[i].x[j]; } sort(a.begin() + 1, a.end(), cmp); vector<vector<int>> lmx(n + 2, vector<int>(m + 2, 0)); vector<vector<int>> rmx(n + 2, vector<int>(m + 2, 0)); for(int i = 1; i <= n; i++){ for(int j = 1; j <= m; j++){ lmx[i][j] = a[i].x[j]; lmx[i][j] = max(lmx[i][j], lmx[i - 1][j]); lmx[i][j] = max(lmx[i][j], lmx[i][j - 1]); } } for(int i = n; i >= 1; i--){ for(int j = m; j >= 1; j--){ rmx[i][j] = a[i].x[j]; rmx[i][j] = max(rmx[i][j], rmx[i + 1][j]); rmx[i][j] = max(rmx[i][j], rmx[i][j + 1]); } } vector<vector<int>> lmin(n + 2, vector<int>(m + 2, 1e9)); vector<vector<int>> rmin(n + 2, vector<int>(m + 2, 1e9)); for(int i = 1; i <= n; i++){ for(int j = m; j >= 1; j--){ rmin[i][j] = a[i].x[j]; rmin[i][j] = min(rmin[i][j], rmin[i - 1][j]); rmin[i][j] = min(rmin[i][j], rmin[i][j + 1]); } } for(int i = n; i >= 1; i--){ for(int j = 1; j <= m; j++){ lmin[i][j] = a[i].x[j]; lmin[i][j] = min(lmin[i][j], lmin[i + 1][j]); lmin[i][j] = min(lmin[i][j], lmin[i][j - 1]); } } PII ans = {0, -1}; for(int k = 1; k < m; k++){ for(int blue = 1; blue < n; blue++){ PII lb = {blue, k}, rb = {blue, k + 1}; PII lr = {blue + 1, k}, rr = {blue + 1, k + 1}; if(lmx[lb.x][lb.y] < lmin[lr.x][lr.y] && rmin[rb.x][rb.y] > rmx[rr.x][rr.y]){ ans = {blue, k}; } } } if(!ans.x) cout << "NO\n"; else{ cout << "YES\n"; string s(n, 'R'); for(int i = 1; i <= ans.x; i++) s[a[i].y - 1] = 'B'; cout << s << " " << ans.y << endl; } } return 0; }

E. Arena ( 2100 )

题意

n 个人,每人有 ai 点生命值 (ai<=k) ,每次每人会对其他所有人造成 1 点伤害。
生命值低于 1 的会死亡,给出 nk ,问有多少种不同初始生命值情况会出现场上无人存活(没有赢家)
输出答案总数 mod998244353

数据范围
2n500
1x500

思路

  • 观察数据范围,唯一有理做法就是 DP,复杂度大概在 O(n3) 左右
  • 首先可以大致确定一个小阶段是进行的轮数,但并不一定用轮数作为一个dp的一个纬度,可能是其他与轮数相关的东西
  • 询问剩余 0 人,多少种不同初始生命值的方案数,这个集合可以由 剩余 0 人,遭受了 j 次攻击的集合组成 (0jx)
  • 状态表示: dp[i][j] 当前轮剩下 i 个人,已经遭受 j 次攻击的情况
  • 考虑如何转移,遍历下一轮剩余的人数为 k,则被更新的状态是 dp[k][min(j + i - 1, x)]
  • 状态转移:
    • dp[k][min(x,j+i1)]=dp[k][min(x,j+i1)]+dp[i][j]Ciik(min(x,j+i1)j)ik
    • i 个人里面选 ik 个人死亡,死亡的 ik 个人里的生命值组成情况可以是 [1,min(x,j+i1)j] 范围。

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; #define x first #define y second #define pb push_back #define mkp make_pair #define endl "\n" using namespace std; const int N = 510, mod = 998244353; int n, x; ll dp[N][N]; // dp[i][j] 当前轮剩下 i 个人,已经遭受 j 次攻击的情况 // 下一轮k个人存活,dp[k][min(x, j + i - 1)] += dp[i][j] * C[i][i - k] * (min(x, j + i - 1) - j) ^ (i-k) ll c[N][N]; // c[a][b] -> C_a^b void init(){ for(int i = 0; i < N; i++) for(int j = 0; j <= i; j++) // b <= a if(!j) c[i][j] = 1; else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod; // % mod } ll qmi(ll a, ll k, int mod){ ll res = 1; while(k){ if(k & 1) res = res * a % mod; a = a * a % mod; k >>= 1; } return res; } void add(ll& a, ll t){ a = (a + t) % mod; } int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cin >> n >> x; init(); dp[n][0] = 1; for(int i = n; i >= 1; i--){ for(int j = 0; j < x; j++){ if(!dp[i][j]) continue; int nxt = min(x, j + i - 1); for(int k = i; k >= 0; k--){ add(dp[k][nxt], dp[i][j] * c[i][i - k] % mod * qmi(nxt - j, i - k, mod) % mod); } } } ll ans = 0; for(int i = 0; i <= x; i++) add(ans, dp[0][i]); cout << ans << endl; return 0; }

Round 117 (Rated for Div. 2)

E. Messages ( 2000 )

题意

现有 n 名学生,老师希望学生 i 阅读消息 mi
老师可以发布 t 条消息,但每个学生只会随机选择其中 ki 条进行阅读,发布信息数量大于 ki 条时,随机选择 ki 条信息阅读。
请给出一种发布信息的方案,使得在期望条件下,有尽可能多的学生 i 阅读了对应的信息 mi

数据范围

1n2105
1ki20
1t2105

思路

  • 从数据范围入手ki 最多只有20个,尝试一下暴力,进行分类讨论
  • 单独看每个信件被阅读到的期望贡献,答案就是 t 个信件期望相加,对于一个学生阅读信件数 ki 来说,被期望阅读该信件,则该信件期望增加 min(ki,t)t
  • 在不考虑 t 的情况下,遍历每个学生,可以获得所有信件的期望,如果 t 能够确定,那么我们只需要取期望前 t 大的信件就是最优的。
  • 如果 t>20,由上可知对于每个信件的期望分母一直变大,由于 ki<20,所以随着 t 变大,最后期望变小,所以毫无意义。
  • 现在考虑 t20,由于随着 t 变大,尽管看似单个信件的期望在变小,信件数量增加,总期望变化不确定,而由于 t 最多只有 20,我们就直接暴力枚举 t 的情况,取最优解即可。
  • 时间复杂度: O(202e5log(2e5))

Solution

#include<bits/stdc++.h> #define pb push_back #define endl "\n" using namespace std; const int N = 2e5; PII pro[N + 10]; bool mp[N + 10]; bool cmp(PII a, PII b){ return a.x > b.x; } int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int n, cnt = 0; cin >> n; vector<PII> a(n); for(int i = 0; i < n; i++){ cin >> a[i].x >> a[i].y; if(!mp[a[i].x]) cnt++; mp[a[i].x] = true; } double mx = 0; vector<int> ans; for(int k = 1; k <= min(20, cnt); k++){ for(int i = 1; i <= N; i++) pro[i] = {0, i}; for(int i = 0; i < n; i++) pro[a[i].x].x += min(a[i].y, k); sort(pro + 1, pro + 1 + N, cmp); double res = 0; vector<int> temp; for(int i = 1; i <= k; i++){ res += pro[i].x; temp.pb(pro[i].y); } res /= k; if(res > mx){ mx = res; ans = temp; } } cout << ans.size() << endl; for(auto t: ans) cout << t << " "; cout << endl; return 0; }

Round 118 (Rated for Div. 2)

D. MEX Sequences ( 1900 )

题意

给定长度 n 的数组 a,其中 0ain 询问合法子序列的个数,合法子序列的定义,对于这个序列的每一个前缀,最后一个数和前缀的 MEX 差值的绝对值不大于 1
答案 mod998244353

数据范围
1n5105
0ain

思路

  • 多画几次可以感觉出大概是个 状态机DP
  • 合法序列的状态只有以下几种:
    1. 0 1 1 2 2 3 4 5
    2. 0 2 2 0 0 2 0 2
    3. 由第一种变为第二种 0 1 2 2 4 2 4 2 4
  • DP阶段为从左往右的数组下标
  • 状态表示: f[i][0] 表示 MEX 为 i 状态为上述第一种的情况, f[i][1] 表示 MEX 为 i 状态为上述第二种的情况
  • 状态转移:i = a[i_]
    • f[i + 1][0] += f[i + 1][0] + f[i][0], 选或不选 + 递增连接。
    • f[i + 1][1] += f[i + 1][1],在第二种情况选或不选
    • if i > 0, f[i - 1][1] += f[i - 1][1] + f[i - 1][0],第二种情况选或不选 + 将第一种情况变成第二种情况
  • 答案统计 Σi=0nf[i][0]+f[i][1]mod998244353

Solution

#include<bits/stdc++.h> #define endl "\n" using namespace std; const int mod = 998244353; void add(ll &a, ll b){ a = (a + b) % mod; } int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int T; cin >> T; while(T--){ int n; cin >> n; vector<vector<ll>> f(n + 2, vector<ll>(2, 0)); vector<int> a(n, 0); for(int i = 0; i < n; i++) cin >> a[i]; f[0][0] = 1; for(int i_ = 0; i_ < n; i_++){ int i = a[i_]; add(f[i + 1][0], f[i + 1][0]); add(f[i + 1][0], f[i][0]); if(i > 0) add(f[i - 1][1], f[i - 1][1] + f[i - 1][0]); add(f[i + 1][1], f[i + 1][1]); } ll ans = 0; for(int i = 0; i <= n; i++){ ans = (ans + f[i][0] + f[i][1]) % mod; } add(ans, mod - 1); cout << ans << endl; } return 0; }

Round 119 (Rated for Div. 2)

D. Exact Change ( 2000 )

题意

3 种货币,每个价值 1,2,3 元,给出 n 个商品的价值,每个价值为 ai ,询问最少的货币数能够准确买下所有商品

思路

  • 此题毒瘤分类讨论,由于最大面值为 3 ,所以 3 元货币尽可能多,贪心地对最贵商品进行讨论,同时价值相同商品无意义排序去重。
  • 设最大商品价值为 x,对 xmod3 模数讨论。
  • 模数为 0,其他商品如果有模数为 1 或者 2 ,用一张 3 元 换成 1 + 2,可以凑出所有方案,否则答案为 x / 3
  • 模数为 1,可以选择 x / 3 - 1 张 3 元 + 2 张 2 元或者 x / 3 张 3 元 + 1 张 1 元,这是最少的答案为 x / 3 + 1
    • 对于前者,不能凑出价值 1 元 和 x - 1 元的商品
    • 对于后者,不能凑出模数为 2 元的商品
    • 所以如果上述两个情况同时成立,ans++
  • 模数为 2,最少的方案是 x / 3 张 3 元 + 1 张 2元 (x / 3 + 1)。如果存在模数为 1 的商品,将一张 3 元拆成 1 + 2即可,ans++

Solution

#include<bits/stdc++.h> #define endl "\n" using namespace std; const int INF = 1e9; int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int T; cin >> T; while(T--){ int n; cin >> n; vector<int> a(n, 0); for(int i = 0; i < n; i++) cin >> a[i]; sort(a.begin(), a.end()); a.erase(unique(a.begin(), a.end()), a.end()); n = a.size(); vector<int> c(3, 0); if(n == 1){ cout << a[0] / 3 + (a[0] % 3 != 0) << endl; continue; } for(int i = 0; i < n - 1; i++) c[a[i] % 3] ++; int tot = (a[n - 1] + 2) / 3; if(a[n - 1] % 3 == 1){ if(c[2] && (a[0] == 1 || a[n - 2] == a[n - 1] - 1)) tot ++; } else if(a[n - 1] % 3 == 2){ if(c[1]) tot ++; } else if(a[n - 1] % 3 == 0){ if(c[1] || c[2]) tot++; } cout << tot << endl; } return 0; }

E. Replace the Numbers ( 1900 )

题意

给一个数组初始为空,进行 q 次操作,一个是向数组末尾插数,一个是将数组中所有的 x 替换乘 y

数据范围
1q5105
1x,y5105

思路

  • 模拟一下,发现每次将 xy 连边,统计最终值就是看 x 连向的终点
  • 换种说法,其实就是并查集的操作,而精妙在于如何处理中途插入
  • 正难则反,倒着操作,遇见插入的时候就把 p[x] 放入答案,每次合并等于并查集合并 p[x] = p[y]
  • 最后将答案逆序输出,时间复杂度 O(n)

Solution

#include<bits/stdc++.h> #define pb push_back #define endl "\n" using namespace std; const int N = 5e5 + 10; int p[N]; struct Q{ int op, x, y; }; int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int m; cin >> m; vector<int> ans; vector<Q> q(m); for(int i = 1; i < N; i++) p[i] = i; for(int i = 0; i < m; i++){ int op, x, y; y = 0; cin >> op; if(op == 1) cin >> x; else cin >> x >> y; q[i] = {op, x, y}; } for(int i = m - 1; ~i; i--){ auto [op, x, y] = q[i]; if(op == 1) ans.pb(p[x]); else p[x] = p[y]; } reverse(ans.begin(), ans.end()); for(auto t: ans) cout << t << " "; cout << endl; return 0; }

Round 120 (Rated for Div. 2)

E. Math Test ( 2200 )

题意

n 个人做 m 道题,知道每个人做题的对错情况。每个人有一个预期得分 xi 和实际得分 rixi 给定,而 ri 是该人做对题目的分数之和。定义 “惊喜度” 为 每个人的 xiri 之和。
请你构造出一个每道题的分数方案,使得:

  • 分数是一个 1m 的排列。
  • 在所有的方案中,该方案的 “惊喜度” 最大。

数据范围
1n10
1m104
0xim(m+1)2

思路

  • 观察数据范围n10,可以二进制枚举每个人的得分绝对值情况,拆绝对值
  • 答案为 Σi=1n(c[i]x[i]c[i]r[i])
  • 前半部分固定,想要答案最大,则后半部分的减数最小,那么对应答对次数小的题目应该获得更高的分数。
  • 枚举不同 c[i] 情况,然后计算每个题目的答对次数(有负数),根据次数排序,从小到大赋值,更新答案即可。
  • 这题时间复杂度比较玄学。按道理就是 O(2nm+2nmlogm),却跑的挺快。

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; #define x first #define y second #define pb push_back #define mkp make_pair #define endl "\n" using namespace std; const int N = 5e5 + 10; int p[N]; struct Q{ int op, x, y; }; int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int m; cin >> m; vector<int> ans; vector<Q> q(m); for(int i = 1; i < N; i++) p[i] = i; for(int i = 0; i < m; i++){ int op, x, y; y = 0; cin >> op; if(op == 1) cin >> x; else cin >> x >> y; q[i] = {op, x, y}; } for(int i = m - 1; ~i; i--){ auto [op, x, y] = q[i]; if(op == 1) ans.pb(p[x]); else p[x] = p[y]; } reverse(ans.begin(), ans.end()); for(auto t: ans) cout << t << " "; cout << endl; return 0; }

Round 125 (Rated for Div. 2)

E. Star MST ( 2200 )

题意

给了一张无向完全图,点数 n 和边权值范围 k,求合法图的数量 mod998244353,合法图的定义是,与 1 号点连接的所有边边权和是 MST 的大小。

数据范围
2n250
1k250

思路

  • 不难看出,主要核心是 1 号点,并且 1 号点连接的边的边权一定是最小的 n1 个,否则不成立和为 MST ,很容易证明
  • 由数据范围看出,应该是个 DP 题目,然后就开始玄学,可知非中心边的边权在 [中心边边权最大值,k] 范围内。
  • DP阶段:加入图中的点数,方案数来源不同边的权值分配不同
  • 状态表示: 根据最终答案是,n 个点,边权最大值为 k 的方案,状态定义为 dp[i][j], 选了 i 个点,最大值为 j 方案数
  • 考虑如何转移,每加入一个新点,会产生 i - 1 条边,加入 i-z 个新点,其中新点定义不同对答案有贡献,贡献为 Ci1z1 , 会产生 (z-1) * (i-z) + (i-z) * (i-z-1) / 2 条新边。
  • 第二维更新,从小到大遍历,jj-1 转移,这样新产生的边不管是中心边还是非中心边,范围都在 [j,k] 之间
  • 状态转移: dp[i][j]=dp[i][j]+dp[z][j1]Ci1z1(kj+1)(z1)(iz)+(iz)(iz+1)/2,答案为 dp[n][k]
  • 时间复杂度:O(n2k)

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; #define x first #define y second #define pb push_back #define mkp make_pair #define endl "\n" using namespace std; const int N = 300, mod = 998244353; ll dp[N][N]; // dp[i][j], 选了i个点,最大值为 j 方案数 int n, k; // dp[i][j] = dp[z][j - 1] * c[i - 1][z - 1] * (k - j + 1)^((z - 1) * (i - z) + (i - z) * (i - z - 1) / 2) // 1 <= z <= i,由dp[z][j - 1] 转移,需要乘上 i-1个点里选z-1个点作为已选择点,新加入的边数 i-z个点和z-1个点连,i-z个点互相连接,每条边的范围在[j,k] int c[N][N]; void init(){ // 递推预处理组合数 for(int i = 0; i < N; i++) for(int j = 0; j <= i; j++) // b <= a if(!j) c[i][j] = 1; else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod; // % mod } // 快速幂计算a^k ll qmi(ll a, ll k, int mod){ ll res = 1; while(k){ if(k & 1) res = res * a % mod; a = a * a % mod; k >>= 1; } return res; } void add(ll &a, ll b){ a = (a + b) % mod; } int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); dp[1][0] = 1; cin >> n >> k; init(); for(int i = 1; i <= n; i++){ for(int j = 1; j <= k; j++){ for(int z = 1; z <= i; z++){ // 按照推导的公式写就行 add(dp[i][j], dp[z][j - 1] * c[i - 1][z - 1] % mod * qmi(k - j + 1, (z - 1) * (i - z) + (i - z) * (i - z - 1) / 2, mod) % mod); } } } cout << dp[n][k] << endl; return 0; }

Round 127 (Rated for Div. 2)

E. Preorder ( 2100 )

题意

给一颗节点数为 2n1 满二叉树,点权为小写字母,根为 1, 可以交换每个节点的两颗子树,问最后对整个树来说会有多少种不同的先序遍历序列(字符串)。

思路

  • 对于叶子节点,只有一种方案
  • 对于非叶子节点,如果两个子树构成不同,该节点数方案为 2 * l * r,否则为 l * r
  • 那么如何判断子树构成是否相同?
  • 将所有子树调整为字典序最大或最小的情况,如 if(s[l] < s[r]) swap(s[l], s[r]), s[u] = " " + s[u] + s[l] + s[r]
  • 然后做一次 DFS 即可求解

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; #define x first #define y second #define pb push_back #define mkp make_pair #define endl "\n" #define ls u << 1 #define rs u << 1 | 1 using namespace std; const int N = (1 << 18) + 10, mod = 998244353; string s[N]; int n; int dfs(int u){ if(u << 1 >= (1 << n)) return 1; ll res = 0; int l = dfs(ls), r = dfs(rs); if(s[ls] < s[rs]) swap(s[ls], s[rs]); if(s[ls] != s[rs]) res = 1ll * l * r * 2 % mod; else res = 1ll * l * r % mod; s[u] = " " + s[u] + s[ls] + s[rs]; return res; } int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cin >> n; string str; cin >> str; for(int i = 1; i < 1 << n; i++){ s[i] = str[i - 1]; } ll res = dfs(1); cout << res << endl; return 0; }

Div3/4

Round #748 div3

D2. Half of Same ( 1900 )

题意

D1的加强版,赛中过了,这里就不再单独将D1列出来。D1题意是输入一个数组 a(1e6ai1e6)n$ 个元素 (4n40,n) 求出一个最大的整数 k,对每个元素减去若干个 k 之后,a中所有元素相同。

此题(D2)的题意与D1大致相同,但求一个最大的 k,使得数组中至少一半的元素能够相同

思路

  • 两道题答案所包含的元素组成的子数组中,设最小值为 v,不难发现,其他元素与v的差值(设为d )一定都对%k 同余。(应该这么表示吧?
  • 由上一点可知,k 一定是所有 d 的最大公约数
  • D1可以求出最小元素 v,再求其他元素相应的 d,D2需要观察数据范围,采用暴力枚举所有元素为 v,再筛选出其他满足条件的 d0,存取所有 d 的约数,找出大于等于 n2 的最大的那个。

自己想法

所有 d%k 同余想到了,还是没深入思考到 k 一定是所有 d 的最大公约数。

Solution

#include<iostream> #include<set> #include<vector> #include<algorithm> #include<map> using namespace std; int main(){ int T; scanf("%d", &T); while(T--){ int n; scanf("%d", &n); int a[45]; for(int i = 1; i <= n; i++) scanf("%d", &a[i]); int ans = -1; for(int i = 1; i <= n; i++){ int v = a[i]; vector<int> nums; int same = 0; for(int j = 1; j <= n; j++){ int d = a[j] - v; if(d == 0) same++; if(d > 0) nums.push_back(d); } if(same >= n / 2){ ans = INT_MAX; continue; } map<int, int> s; for(int j = 0; j < nums.size(); j++){ int & d = nums[j]; for(int g = 1; g <= d / g; g++){ if(d % g == 0){ s[g]++; if(d / g != g) s[d / g]++; } } } for(auto t: s){ if(t.second >= n / 2 - same){ ans = max(ans, t.first); } } } if(ans == INT_MAX) ans = -1; printf("%d\n", ans); } return 0; }

Round #780 div3

F2. Promising String ( 2100 )

题意

给了长度为 n 的字符串,由 '+' 和 '-' 组成,可以将字符串中两个 '+' 换成 1 个 '-',询问合法子串个数,合法子串定义,子串内 '+' 数量和 '-' 数量相同。

数据范围
1n2105

思路

  • F1 n 只有 3000,是一个暴力,F2考虑优化。
  • 我们只关心区间中 '-' 个数与 '+' 个数的差值,自然 '+' 个数比 '-' 小,多模拟一下发现一个简单结论:区间中两数差值 mod3=0 时,区间合法。
  • 因为是一个区间计数问题,我们可以考虑前缀和思想来做,如果两个前缀模 3 同余,那么两个前缀间的区间便合法。
  • 对于这样的问题,由于需要统计 3 个模数的情况,并且不好用单一前缀和数组来统计小于 pre[i] 的所有合法前缀和,考虑用树状数组来处理
  • 所以开 3 个树状数组,tr[i] 表示模数为 i 的前缀和的树状数组,这样统计答案就很方便了。
  • 由于做减法会有负数存在,树状数组只能存正数。所以需要值域整体右移,然后需要一点细节,具体看代码注释。
  • 时间复杂度 O(nlogn)

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; #define x first #define y second #define pb push_back #define mkp make_pair #define endl "\n" using namespace std; // 结论:区间内 +号个数 减去 -号个数 % 3 == 0,区间合法,差一定为正数。 struct BIT { int n; vector<int> B; BIT(){}; BIT(int _n) : n(_n), B(_n + 1) {} void init(int _n){ n = _n; B.resize(_n + 1); } inline int lowbit(int x) { return x & (-x); } void add(int x) { for(int i = x; i <= n; i += lowbit(i)) B[i] += 1; } int ask(int x) { int res = 0; for(int i = x; i; i -= lowbit(i)) res += B[i]; return res; } }; int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int T; cin >> T; while(T--){ int n; cin >> n; string s; cin >> s; s = " " + s; vector<int> pre(n + 1, 0); vector<BIT> tr(3, BIT(2 * n + 2)); // 需要加 n + 1, 单单 2 * n 不太够 for(int i = 1; i <= n; i++) pre[i] = pre[i - 1] + (s[i] == '-' ? 1 : -1); for(int i = 1; i <= n; i++) pre[i] += n + 1; // 可能为-n,加上n+1才能是正数 tr[(n + 1) % 3].add(n + 1); // n+1成了"零"的位置 ll ans = 0; for(int i = 1; i <= n; i++){ int c = pre[i] % 3; ans += tr[c].ask(pre[i]); tr[c].add(pre[i]); } cout << ans << endl; } return 0; }

Round #787 div3

F. Vlad and Unfinished Business ( 1800 )

题意

给了点数为 n 的树,和 k 个必须到达的点,出发的起点 x 和 到达终点 y,边权为 1 ,询问从起点经过指定的
k 个点,最后到达 y 的最短路径花费, k 个点的访问顺序可以任意。

数据范围
1kn2105

思路

  • 贪心地想,对于所有任务点必须到,然后要返回,那么起点到这些点的路径和至少为2倍到达这些点的花费。
  • 问题转化为,经过 k 个点的"最小生成树",当然这里不需要最小生成树算法,只需要知道我们不需要经过的点数即可,剩余点数为 x,路径长度为 x1
  • 那么现在访问了所有点,要更贪心的想,从 x>y 我们是不是只用走一次就行了,先把 y 以外的点搜完,最后来搜 y 的子树即可,那么 path:x>y 只需要经过一次。
  • 所以最后答案为 2(1)x>y ,用 dfs 求子树标记的方式来解,详细见代码,时间复杂度 O(n)

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; #define x first #define y second #define pb push_back #define mkp make_pair #define endl "\n" using namespace std; const int N = 2e5 + 10; int n, k, x, y, ans; vector<int> edge[N]; // 邻接表存图 int dep[N]; bool st[N]; int dfs(int u, int p){ int tot = 0; for(auto v: edge[u]){ if(v == p) continue; dep[v] = dep[u] + 1; // 更新深度 tot += dfs(v, u); } if(!tot && !st[u]) ans--; // 不是必经点 return tot + (st[u] == true); // 传递标记 } int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int T; cin >> T; while(T--){ cin >> n >> k; cin >> x >> y; ans = n; vector<int> a(k + 1, 0); for(int i = 1; i <= k; i++){ cin >> a[i]; st[a[i]] = true; // 必须经过的点 } st[x] = true, st[y] = true; for(int i = 1; i < n; i++){ int u, v; cin >> u >> v; edge[u].pb(v); edge[v].pb(u); } dfs(x, -1); cout << 2 * (ans - 1) - dep[y] << endl; // 算答案 for(int i = 1; i <= n; i++){ edge[i].clear(); st[i] = false; dep[i] = 0; } } return 0; }

G. Sorting Pancakes ( 2300 )

参考题解: Ximena, 严格鸽

题意
给了一个长度 n 的数组 a ,和保证为 m, 每次只能将 ai 减 1, ai1orai+1 加 1,询问最小的操作次数使 a 非严格递减。

数据范围
1n,m250

思路

  • 毫无疑问,一道dp题,根据数据范围,大概可以容忍 O(n3) 级别的时空复杂度。
  • 在设计状态前,务必需要一个结论
    • 对于这样的相邻移动元素的操作而言,设操作后的结果为 b 数组,总操作次数等于 Σi=1n(|Sb[i]Sa[i]|) ,对应前缀和差的绝对值的和
  • 首先dp的阶段是长度 i,由于数组和固定,考虑加入一维当前数组的和 j ,由于要满足递增或者递减,加入一维记录最后一个数 k ,由于需要对比大小,这个数其实是一个最值。
  • 那么状态已经设计出来了: dp[i][j][k] 表示数组前 i 位,和为 j ,最后一位最值为 k 的最小操作次数。
  • 考虑如何转移方便,如果按照题意原本描述来看,转移方程是遍历前一个数 x,kx dp[i][j][k] = min(dp[i][j][k], dp[i - 1][j - k][x] + abs(j - s[i])),观察发现可以使用最小后缀来优化 x 的遍历,此时预处理最小后缀复杂度为 O(n3)
  • 而考虑反着做,即将数组翻转后,考虑将数组构造成非严格递增。那么 k 代表的是最后一位的最大值。从小到大枚举k 时,需要的是最小前缀,可以很方便的记录, 如下代码所示
for(int i = 1; i <= n; i++){ for(int j = 0; j <= m; j++){ int mi = 0x3f3f3f3f; for(int k = 0; k <= m - j; k++) { mi = min(mi, dp[i - 1][j][k]); // 转移 k 时,mi 记录了上一位数值为 [0, k] 的最小值,因此是合法的 f[i][j + k][k] = min(f[i][j + k][k], mi + abs(s[i] - abs(j + k))); } } }
  • 时间复杂度:将 nm 同级,O(n3) ,翻转数组的方法还没有想到如何优化成 O(n2logn)
    • 如果尝试采用记录后缀最小来优化,发现对于下标 i 上每个数不能超过 m / i ,否则它一定比前面的某个数大(平均值),加入循环条件中可以做到 O(n2logn)

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; #define x first #define y second #define pb push_back #define mkp make_pair #define endl "\n" using namespace std; const int N = 260; int n, m; int a[N], s[N]; int dp[N][N][N]; // 将数组翻转 // dp[i][j][k] 前i个数,和为j,最后一个数最大值为k 满足递增序列最小操作 int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cin >> n >> m; for(int i = n; i >= 1; i--) cin >> a[i]; for(int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i]; memset(dp, 0x3f, sizeof dp); dp[0][0][0] = 0; // 前0个数,和为0,数最大值为0,操作次数为0 for(int i = 1; i <= n; i++){ for(int j = 0; j <= m; j++){ int mi = 0x3f3f3f3f; for(int k = 0; k <= m - j; k++){ mi = min(dp[i - 1][j][k], mi); dp[i][j + k][k] = min(dp[i][j + k][k], mi + abs((j + k) - s[i])); } } } int ans = 0x3f3f3f3f; for(int i = 0; i <= m; i++) ans = min(ans, dp[n][m][i]); cout << ans << endl; return 0; }

Round #797 div3

D. Black and White Stripe ( 1000 )

固定长度最大子段和, 简单只贴代码了

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; typedef double db; #define arr(x) (x).begin(),(x).end() #define x first #define y second #define pb push_back #define mkp make_pair #define endl "\n" using namespace std; int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int T; cin >> T; while(T--){ int n, k; cin >> n >> k; vector<int> pre(n + 1, 0); string s; cin >> s; s = " " + s; for(int i = 1; i <= n; i++){ pre[i] = pre[i - 1] + (s[i] == 'B'); } int ans = n; for(int i = k; i <= n; i++){ ans = min(ans, k - (pre[i] - pre[i - k])); } cout << ans << endl; } return 0; }

E. Price Maximization ( 1500 )

双指针, 贪心, 排序, 或者 STL

题意

给了 n 个货品, n 是偶数, 每个货品有价值 ai , 将货品两两配对, 两两配对和总价值为 wi, 给定 k, 求 Σwik 最大值

数据范围
2n2105
1k1000
0ai109

思路

  • 观察数据范围 , k1000.
  • 由于答案为重量之和向下取整, 对 ai 来说, 无论怎样组合, ai/k 的贡献是确定的.
  • 这样我们只用关心 ai%k 的贡献, 贪心 地来想, 对于 x=ai%k , 需要找到满足最小余数 kx
  • 可以用两种方式实现
    • 一种是显然的双指针 (呵呵我写挂了, 很离谱, 有时间来补这个解法)
    • 还有一种是 jiangly超人 的 STL 做法(复杂度稍高但聊胜于无)

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; typedef double db; #define arr(x) (x).begin(),(x).end() #define x first #define y second #define pb push_back #define mkp make_pair #define endl "\n" using namespace std; int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int T; cin >> T; while(T--){ int n, k; cin >> n >> k; ll ans = 0; multiset<int> s; for(int i = 0; i < n; i++){ int x; cin >> x; ans += x / k; s.insert(x % k); } while(!s.empty()){ auto x = *s.begin(); s.erase(s.begin()); auto it = s.lower_bound(k - x); if(it != s.end()){ s.erase(it); ans++; } } cout << ans << endl; } return 0; }

F. Shifting String ( 1700 )

置换, 环, 字符串最小循环节, LCM

题意

给定一个长度为 n 的字符串 s, 和一个排列 P , 每次操作让 si=sPi , 询问至少经过多少次操作能让字符串与初始状态相同

数据范围
1n200
1pin

思路

  • 草稿纸上画几遍, 发现可能与置换中所形成的各个环的长度有关系, 这个关系是各长度的 LCM
  • 答案其实是与每个环经过多少次变换能变成原来的字符串, 由于是字符串关系, 这个多少次变换, 其实就是每个环上对应字符串的最小循环节.
  • 所以答案就是各个环最小循环节长度的 LCM
  • 如何求最小循环节? 此题可以暴力枚举循环节长度判断是否合法, 数据规模较大可以使用 KMP 求字符串最小循环节, 得到 ne 数组判断 if(len % (len - ne[len]) == 0)

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; typedef double db; #define arr(x) (x).begin(),(x).end() #define x first #define y second #define pb push_back #define mkp make_pair #define endl "\n" using namespace std; const int N = 210; ll gcd(ll a, ll b){ return b ? gcd(b, a % b) : a; } ll LCM(ll a, ll b){ return a / gcd(a, b) * b; } int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int T; cin >> T; while(T--){ int n; string s; cin >> n >> s; s = " " + s; vector<bool> st(n + 1, false); vector<int> p(n + 1,0); for(int i = 1; i <= n; i++) cin >> p[i]; ll ans = 1; for(int i = 1; i <= n; i++){ if(st[i]) continue; string tmp = " "; int sz = 0; for(int j = i; !st[j]; j = p[j]){ st[j] = true; tmp += s[j]; sz++; } vector<int> ne(sz + 1, 0); ne[1] = 0; for(int i = 2, j = 0; i <= sz; i++){ while(j && tmp[j + 1] != tmp[i]) j = ne[j]; if(tmp[i] == tmp[j + 1]) j++; ne[i] = j; } ll len = sz - ne[sz]; if(sz % len == 0) ans = LCM(ans, len); else ans = LCM(ans, sz); } cout << ans << endl; } return 0; }

G. Count the Trains ( 2000 )

STL, 二分, 思维

题意

给了 n 个点, 每个点有值为 ai , 从左往右形成序列, 对于序列上下标 i 位置, 值为 min{aj},(ji)

m 次操作, 输入 k,d , 每次操作对 a[k] -= d , 询问每次操作完之后, 序列中有多少个不同的数 ?

思路

  • 容易挖掘题目性质, 对于一个数 ai , 如果 j<i,ajai , 那么 ai 不会对答案产生贡献
  • 由于答案是求不同的数个数 , 我们可以联想到 map, set 这一类 STL 容器来解决, 那么问题是如何实现.
  • 参照第一点, 我们尝试用 map<int,int> 容器来实现, 第一关键字为下标, 第二关键字为对应值
  • 每次输入或操作对 map 中插入 i, a[i], 若前面迭代器元素值小于等于 ai 则删除现在插入的迭代器, 若没有删除, 检查后面的迭代器是否满足要求(next(it)->second <= it->second)
  • 实现过程中注意代码细节, 防止越界等错误, 时间复杂度为 O(n+m)log(n+m)) , 参考 yyds的jiangly

Solution

#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; typedef double db; #define arr(x) (x).begin(),(x).end() #define x first #define y second #define pb push_back #define mkp make_pair #define endl "\n" using namespace std; map<int, int> mp; void add(int i, int x){ mp[i] = x; auto it = mp.find(i); if(it != mp.begin() && prev(it)->second <= it->second){ mp.erase(it); return ; } while(next(it) != mp.end() && next(it)->second >= it->second) mp.erase(next(it)); } int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int T; cin >> T; while(T--){ mp.clear(); int n, m; cin >> n >> m; vector<int> a(n + 1, 0); for(int i = 1; i <= n; i++){ cin >> a[i]; add(i, a[i]); } while(m--){ int k, d; cin >> k >> d; a[k] -= d; add(k, a[k]); cout << mp.size() << " "; } cout << endl; } return 0; }

野场

3000-3500 #932 9e 35 25
2600-2900 #E54 ed 55 46
2400-2500 #F87 ee 75 6e
2300 #FB5 f5 be 5e
2100-2200 #FA8 f7 cd 8a
1900-2000 #F9F f3 90 ff
1600-1800 #AAF ab a9 ff
1400-1500 #8DB 8d d9 ba
1200-1300 #9F7 95 fc 77
800-1200 #CCC cb cb cb


__EOF__

本文作者Roshin
本文链接https://www.cnblogs.com/Roshin/p/Codeforces_Solution.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   Roshin  阅读(584)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
-->
点击右上角即可分享
微信分享提示