Codeforces 题解集合
大概只会放 *2000 以上的题目上去了
[Change Log]
created on 2022.5.6
latest updated on 2022.12.6
- Div1
- Div1 + Div2 & Global Round
- Div2
- Education Round
- Round 42 (Rated for Div. 2)
- Round 47 (Rated for Div. 2)
- Round 84 (Rated for Div. 2)
- Round 115 (Rated for Div. 2)
- Round 116 (Rated for Div. 2)
- Round 117 (Rated for Div. 2)
- Round 118 (Rated for Div. 2)
- Round 119 (Rated for Div. 2)
- Round 120 (Rated for Div. 2)
- Round 125 (Rated for Div. 2)
- Round 127 (Rated for Div. 2)
- Div3/4
- 野场
Div1
先留着(
Div1 + Div2 & Global Round
Global Round 20
D. Cyclic Rotation ( \(\color{#AAF}{1700}\) )
构造,双指针, multiset,逆向,等价变换
题意
给定长度为 \(n\) 的数组 \(a\),和 \(a\) 的一个排列数组 \(b\),对 \(a\) 进行操作:
- 如果 \(a_l =a_r\),则 \([a_l,...,a_r] = [a_{l+1},...,a_r,a_l],\; 1\leq l<r\leq n\)。
询问能否对 \(a\) 进行任意次操作得到 \(b\)
数据范围
\(1\leq n \leq 2 * 10^5\)
\(1\leq a_i,b_i \leq n\)
思路
- 对操作进行等价变换,每次对 \(a_l,a_r\) 的操作相当于把 \(a_l\) 删除, 在 \(a_r\) 前拷贝一份 \(a_r\) 。
- 尝试逆向,我们看能否从 \(b\) 得到 \(a\)。
- 双指针构造方案:
- \(i,j\) 指向 \(a,b\) 最后一个元素,维护一个
multiset
while (b[j] == b[j - 1]) j--;
,向 multiset 中插入 \(b_j\)- 如果 \(a_i = b_j\) ,
i--,j--
- 否则,在 multiset 中查找 \(a_i\) ,找到则
i--
否则无解。
- \(i,j\) 指向 \(a,b\) 最后一个元素,维护一个
- 因为每次操作,\(a_r\) 的位置其实相对是不变的,然后根据第一点,我们从右往前查 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 ( \(\color{#F9F}{1900}\) )
构造,分治,或数据结构贪心
题意
给定长度为 \(n\) 的排列,有 \(n\) 个点,两点 l, r 之间当且仅当下标区间在 \([l,r]\) 中 \(a[l], a[r]\) 为区间的两个最值,可以连边,询问从 \(1-n\) 的最短路。
数据范围
\(1\leq n \leq 2.5*10^5\)
思路
- 抓住题目性质,先找到区间最大值的位置 \(pos\) ,假设 \(pos\neq1,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 (\(\color{#FA8}{2200}\))
状压DP,简单计算几何
题意
在二维坐标上,一个人从 \((l,0)\) 走到 \((r,0)\) ,有 \(n\) 盏路灯,每盏灯的照射角度最大为 \(a_i\),坐标为 \((x_i,y_i)\)。询问这个人最多能走多远,到 \(r\) 停止。
数据范围
\(1\leq n\leq 20\)
\(-10^5 \leq l\leq r \leq 10^5\)
\(-1000\leq x_i\leq 1000, 1\leq y_i \leq 1000, 1\leq a_i\leq 90\)
思路
- 观察数据范围, \(n\leq 20\) ,两种想法,选和不选和二进制枚举,自然是二进制枚举。
- 涉及二进制枚举,一个很自然的想法是状压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(x_b,0)\) 对应的角,然后用其补角 tan 求出 \(x - x_b\),进而求出 \(x_b\) - 也可以用旋转矩阵,先求出向量 \((dx,dy)=(f[i]-x,-y)\) 做逆时针旋转矩阵新的向量 \((dx,dy)=(dx * cos(a) - dy*sin(a),dx*sin(a)+dy*cos(a))\) ,那么 \(x_b = x - y * dx/dy\)
- 两种情况都需要特判,最远点是无穷大或者其他边界问题。
- 可以用
- 目标答案为 \(f[(1<<n)-1]\) ,时间复杂度为 \(O(n*2^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;
/*----------------------------------------------------------------------------------------------------*/
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 (\(\color{#FB5}{2300}\))
矩阵快速幂
题意
在大小为 \(n\times n\) 的棋盘上,一个点起始坐标为 \((sx,sy)\) 起始速度是 \((dx,dy)\) ,询问经过 \(t\) 秒后的位置,对于每一秒:
- 点处于 \((i,j)\),给速度的贡献为 \(k\), 速度将变为 \((dx+k, dy+k)\)
- 移动到 \((x+dx-1, y + dy-1)\), 这里的 \(dx,dy\) 已经更新
- 每个点给速度的贡献 \(+1\)。
数据范围
\(1\leq n \leq 10^9\)
\(1\leq sx,sy \leq n\)
\(-100\leq dx,dy \leq 100\)
\(0\leq t \leq 10^{18}\)
思路
- 观察数据范围,\(t\leq 10^{18}\) 很大很大,相当于询问你经过 \(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\)
- 故状态矩阵为
\[ \begin{bmatrix} 1 & 0 & 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 1 & 0 & 0 \\ 1 & 1 & 2 & 1 & 0 & 0 \\ 1 & 1 & 1 & 2 & 0 & 0 \\ 1 & 1 & 1 & 1 & 1 & 0 \\ 2 & 2 & 2 & 2 & 1 & 1 \\ \end{bmatrix} \] - 时间复杂度为 \(O(6^3 * logt)\)
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 ( \(\color{#F9F}{1900}\) ) (结论、计数)
题意
给了一颗 \(n\) 个节点的树,询问存在多少种合法排列,使得 \(n\) 个点在圆环上排列对应位置上,相连的边不会有相交。
答案模 \(998244353\)
数据范围
\(2\leq n \leq 2 * 10^5\)
思路
- 核心点:一个结论:一个节点的子树所有点在圆环上连续的一段。
- 所以一个节点的子节点可以有阶乘级别的排列。
- 更正式的表达,\(f[u] = ((u != root) + son[u]!) * \Pi_{v\in son[u]}{f[v]}\)
- 由于整棵树根可以随意设置,但最后效果其实和 \(1\) 放在 \(n\) 个位置上的效果一样, 所以最后答案等于 \(n * f[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 ( \(\color{#9F7}{1200}\) )
题意
输入长度为 \(n\) 的字符串 \(s\) 和一个字符 \(c\) ,可以进行一个操作,对于 \(1≤x≤n\) ,可以将不能整除 \(x\) 的 \(i(1≤i≤n)\) ,所在的下标字符替换成 \(c\) ,问最少经过多少次操作可以使 \(s\) 每个字符都是 \(c\) ?
思路
- 由整除出发,
- 对于 \(x=n\) 而言,\(1≤i≤n-1\) 都可以被替换。
- 对于 \(x=n-1\) 而言,\(i=n\) 可以被替换。
- 因此最多需要2次操作就可以将 \(s\) 替换成想要的字符串。
- 对于做 \(0\) 次操作,遍历数组就行。
- 对于做 \(1\) 次操作,当数组中\(\exists i<n\),\(i\) 及 \(i\) 的倍数下标位置的字符都是 \(c\) 的话,只需要一次操作就可以把 \(s\) 都替换成 \(c\) ,输出 \(i\) 的下标。
- 对于做 \(2\) 次操作,就是 \(\nexists i<n\) ,\(i\) 及 \(i\) 的倍数下标位置的字符都是 \(c\) ,输出 \(n-1\) 和 \(n\)。
赛中
上述的第一点想到了,但是忽略了做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 ( \(\color{}{1900}\) )
构造、逆序操作
题意
求一个长度为 \(n\) 的 01 数组 \(A\),对 \(A\) 进行 \(n\) 次操作, 每次对 \([1,n]\) 的区间进行排序结果累加到 \(C\) 数组中,已知 \(C\) 数组求 \(A\) 数组。
数据范围
\(1\leq n \leq 2 * 10^5\)
\(0\leq c_i \leq n\)
思路
- 观察1:将 \(C\) 数组求和 \(/n\) 可以得到 \(1\) 的个数 \(num\)。由贡献思考
- 观察2:如果 \(C_n=n/1\) ,那么 \(A_n = 1/0\) 。
- 由观察1得到 1 的个数,抓住题目性质,在第 \(n\) 次操作时,是由后缀为 \(num\) 个 \(1\) 的数组累加。\(A_n\) 已经求出,看前 \(n-1\) 位,如果我们把第\(n\) 次的后缀累加给撤销,得到的 \(C[1,n-1]\) 其实是第 \(n-1\) 次操作累加之后的 \(C\) 的前 \(n-1\) 位,这时候可以不用管 \(A_n,C_n\) 。然后同理如果 \(C_{n-1}=n-1/1,\; A_{n-1} = 1/0\) 。就这样往前递推。
- 但需要注意,如果该位为 \(1\) ,\(num\) 需要减 1,很好理解,在 \(i\) 之前 \(A_i=1\) 没有在排序范围内自然也不属于后缀。
- 涉及区间更新操作就懒得 \(O(n)\) 搞来搞去了,直接上差分树状数组,复杂度 \(O(n*logn)\)。
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 ( \(\color{#FA8}{2200}\) )
构造
题意
给了 \(n\) 个点的树, \(n\) 是 \(2\) 的幂次,边权和点权是 \([1, 2 * n - 1]\) 的排列。询问从任意点为根,从根出发途中经过的的点和路径权值 \(异或\) 最大值的最小值是多少?
数据范围
\(1\leq p\leq 17\)
\(2^p = n\)
思路
- 首先如果单独选一个大于 \(2^n\) 的点为根,最大值 \(\geq 2^p\) ,所以考虑根权值为 \(2^p\) ,并且途径最大值为 \(2^p\) ,其实就是一个构造
- 接下来需要 \(n-1\) 对数分别为 \(x, x + 2^p\)
- 分类讨论构造一下
- 父节点 \(>= 2^p\),子节点值为 \(x\) ,边为 \(x + 2^p\)
- 父节点 \(< 2^p\),子节点值为 \(x + 2^p\),边为 \(x\)
- 这样构造到根的子节点,异或和为0,而由于根大(指大于 \(2^p\)) ,那么子节点小,到了子节点到子节点的边异或值为 \(x\),到了子节点的子节点异或值为 \(2^p\) ,如此循环往复,所有值一定 \(\leq 2^p\)
#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 ( \(\color{#AAF}{1700}\) )
DP + 行列分开计算贡献
题意
往 \(n*m\) 矩阵左上角里塞 0 和 1, \(m\) 列的元素前往下一行第1列, 依次后移, 询问每一次行和列中有多少个行和列里有 \(1\)
数据范围
\(n*m\leq 10^6\)
思路
- 行列分开考虑.
- 对于列, 进入 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 (\(\color {#AAF}{1800}\))
题意转换, 单调栈, 前缀和, 最值查询
题意
给了一个长度为 \(n\) 的数组 \(a\), 询问该数组是否满足任意连续子序列 \([i, j]\) 满足:
数据范围
\(1\leq n \leq 2 * 10^5\)
\(-10^9\leq a_i \leq 10^9\)
思路
- 题意转换: 实际询问从 \(a_i\) 向两边扩展, 在未达到更新最大值的位置 \([l,r]\), 考虑一般情况下, 前缀和 \(pre[r] - pre[l - 1] > a[i]\) 是否成立
- 以上转换, 可以把问题拆分多个问题空间的不重不漏的子集.
- 显然需要用单调栈\笛卡尔树 求出左右两边大于 \(a_i\) 的第一个下标分别记为 \([L,R]\)
- 则第一点的式子转化为是否存在 \(l,r(L<l<i, i< r<R)\) 满足 \(sum[l, i -1] + sum[i + 1, r] + a[i] > a[i]\).
- 即 \(sum[l, i-1] + sum[i+1, r] > 0\) , 仔细观察发现等式只需要只要其中一个和大于 \(0\) 就能满足了
- 故问题转化为求是否 \(max{sum[l, i-1]} > 0 \; or\; max{sum[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 ( \(\color{#AAF}{1700}\) )
方格dp、思维
题意
询问在大小为 \((n, m)\) 的方格中,每个格子上值为 \(-1\) 或 \(1\),是否存在一条从 \((1,1)\) 到 \((n, m)\) 的路径,满足路径和为 \(0\)
数据范围
\(1\leq n,m \leq 1000\)
思路
- 这个题很重视观察性质。
- 如果 \(n + m\) 为偶数,则最后一定经过 奇数 个格子,此时无解。 那么一定经过偶数个格子。
- 明显可以通过 dp 预处理出到达每个格子的最大值和最小值。如果 \(dp[n][m]\) 的 \([最小值,最大值]\) 包含 \(0\) 则有解,否则无解,证明:
- 如果不包含显然无解。证明包含一定有解。
- 因为经过偶数个格子,最后的和一定是偶数,(奇数-奇数,偶数-偶数)
- 如果进行一次调整让和变化,一定是 \(2,0,-2\) 。因为本来和就是偶数,所以只要区间包含 0, 一定可以调整到和为 0, 证毕。
- 时间复杂度 \(O(n*m)\) ,代码上还需要一些细节。一道不错的题。
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 ( \(\color{#AAF}{1700}\) )
差分、构造
题意
给了长度为 \(n\) 数组 \(a\),可以进行三种操作:
- 对任意前缀所有元素 \(-1\) .
- 对任意后缀所有元素 \(+1\) .
- 对全部元素 \(=1\) .
询问最少经过多少次操作可以将所有元素置 \(0\)
数据范围
\(1\leq n \leq 200000\)
\(1\leq a_i \leq 10^9\)
思路
- 由于是区间操作,不妨用差分来思考
- 构造差分数组
b[i] = a[i] - a[i - 1]
, 目标状态 \(b[i],\; i\leq n\) 全为 \(0\) - 对于三种操作,等价于对差分数组:
b[1]--, b[i>1]++
b[n + 1]++, b[i<=n]--
b[1]++
- 所以先将 \(2 \leq i \leq n\) 的所有 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 ( \(\color{#FA8}{2200}\) )
贪心
题意
给了 \(n\,(2\leq n\leq 2*10^5)\) 个点,坐标 \(-10^9 \leq x \leq 10^9\) 。每个点属于 P,B,R
其中一个。
询问在 {P,B}
点集中连接所有点和 {P,R}
点集中连接所有点,两者加起来的花费最小值。
思路
- 一点点考虑发现是个贪心分类讨论题。
- 对于没有 P 点存在,答案很显然。
- 如果两个 P 点中夹杂 B/P。对于这一段连接方式有两种。
- 相邻两点不管什么颜色直接连接。
- 先连接这一段的两个端点 P,这样在上一种连接方式下,可以扣除中间跨度最大的相邻点的连线,这样可能会更优。
- 时间复杂度 \(O(n)\)。
Solution
Round 47 (Rated for Div. 2)
这场vp, 打的很烂, D 题读漏条件, B 题贪错方向
B. Minimum Ternary String ( \(\color{#8DB}{1400}\) )
贪心
题意
给定长度为 \(n(1\leq n\leq 10^5)\) 的串, 只有 \(0,1,2\) 组成, 相邻 \(0\) 和 \(1\) 可以互换, 相邻 \(1\) 和 \(2\) 可以互换, 询问任意互换次数后整个串的最小字典序
思路
- 主要把握住题目条件, 发现 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 ( \(\color{#AAF}{1700}\) )
贪心---货仓选址模型利用, 注意开 long double
题意
输入 \(n,m\) , 表示长度为 \(n\) 的数组, \(m\) 次操作, 每次操作有 \(x\) 和 \(d\), 每次操作选择一个下标 \(i\), 对数组所有元素加上 \(x + d * |i-j|\), 询问最终操作后数组平均值最大值
数据范围
\(1\leq n, m \leq 10^5\)
\(-10^3\leq x_i,d_i\leq 10^3\)
思路
- 平均值最大 -> 数组和最大, 负数影响最小, 正数影响最大.
- \(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 ( \(\color{#AAF}{1700}\) )
构造, 边上两点编号互质, 图要求连通
题意
给定一个图, 询问是否能构造出 \(n\) 个点, \(m\) 条边的连通图, 任意边两端点编号互质
数据范围
\(1\leq n,m\leq 100000\)
思路
- 观察数据范围, 最多只有 \(m, 1000000\) 条边, 对于每一个数与其互质小于它的数字个数为 \(\Phi (x)\), 累加到 \(600\) 左右时, 已经大于了 \(10^5\)
- 故可以暴力用
gcd()
判断两点互质来连边 - 记得是连通图, 即 \(m > n - 1\)
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 ( \(\color{#F9F}{2000}\) )
组合数, 递推, 期望
题意
你要从 \(0~n\) ,有 \(n\) 个站点 \((1~n)\),每个站点都有 \(1/2\) 的几率有休息点。你连续坐 \(k\) 站时,每两站间的疲劳值为 \(a_1,a_2……a_k\),
如果第 \(k\) 站有休息点,那么你可以在此处休息,然后接下来的站点的疲劳值又从 \(a_1\) 开始。求 \(p(疲劳值的期望)*2^(n-1)\)。
数据范围
\(1\leq n,a_i\leq 10^6\)
思路
- 我们要求的其实是疲劳值之和, 直接算肯定不好搞, 要按贡献来算, 有两种思路, 考虑每两点之间的答案贡献, 考虑每个 \(a_i\) 的贡献, 前者请参考官方题解
- 计算每个 \(a_i\) 的贡献其实是求每个 \(a_i\) 的出现次数
- 考虑 \(a_1\) , 由于 \(0\) 点的特殊性, \(0->1\) 一定有一个 \(a_1\), 无论后面怎么走, 有 \(2^{n-1}\) 次, 考虑后面的 \(n\) 个点相邻两点 x 和 y, 一定是 x 休息, 其他点无所谓(但不考虑终点的休息)有 \((n-1)*2^{n-2}\)
- 同样考虑 \(a_2\), 对于 \(0\) 点特殊考虑, \(0->1->2\), 2 及以后的店怎么搞无所谓, 有 \(2^{n-2}\) 次, 同样和前面一样, x, y, z 三个点, 一定是 x 休息, y 不休息, 其他点无所谓, 有 \((n-2)*2^{n-3}\)
- 所以对于任意 \(a_i\) 出现次数为 \(2^{n-i} + (n-i)*2^{n-i-1}\)
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 ( \(\color{#FA8}{2200}\) )
置换环、数论
题意
定义排列乘法 \(c=a\times b\) , \(c[i]=b[a[i]]\) 。自然有排列的幂次 \(p^k = p*p*\cdots *p\)。
给定在长度为 \(n\,(1\leq n \leq 2 \times 10^5)\) 的排列 \(p\) 。和每个点的颜色 \(c\) 。询问最小的幂次 \(k\) 能存在一个
\(i\) 使得 \(i,p[i],p[p[i]],p[p[p[i]]],\dots\) 都是同一种颜色。
思路
- 读完题目很容易联想置换环这个东西,每个点 \(i\) 向 \(p[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 ( \(\color{#FA8}{2100}\) )
经典放格计数, DP 预处理, 修改只考虑单点对答案的贡献
题意
一张 \(n * m\) 的图,有两种楼梯形,L
和 7
形,即向下向右和向右向下这样无限重复,单点和横着的两个点和竖着的两个点也算,有 \(q\) 个询问,每次将每个点的权值异或 \(1\) ,如果某个点的值为 \(0\) ,那么这个点就不能用,再询问贡献。
数据范围
\(1\leq n \leq 1000\)
\(1\leq m \leq 1000\)
\(1\leq q \leq 10^4\)
思路
- 先处理所有格子都 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 ( \(\color{#F87}{2400}\) )
题意
给了一个 \(n*m\) 的矩阵,每个元素有一个值,现在要把矩阵的行染成蓝色或红色(必须两个颜色都有),染完后将矩阵切成左右两个矩阵,切割行为 \(k\)
左边矩阵红色元素都比蓝色元素大,右边矩阵蓝色元素都比红色元素
数据范围
\(2\leq n,m \leq 5\times 10^5\; , n*m \leq 1e6\)
\(a_{ij} \leq 10^6\)
思路
- 首先无论分割点在哪里,第一列永远要被考虑,如果将行按第一列元素大小排序,那么永远只有前缀行染成蓝色
- 然后枚举切割点和染色行检验合法性,要预处理顶点出发的子矩阵最值做到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 ( \(\color{#FA8}{2100}\) )
题意
有 \(n\) 个人,每人有 \(a_i\) 点生命值 (\(a_i <= k\)) ,每次每人会对其他所有人造成 \(1\) 点伤害。
生命值低于 \(1\) 的会死亡,给出 \(n\) 和 \(k\) ,问有多少种不同初始生命值情况会出现场上无人存活(没有赢家)
输出答案总数 \(\mod 998244353\)
数据范围
\(2\leq n \leq 500\)
\(1\leq x \leq 500\)
思路
- 观察数据范围,唯一有理做法就是 DP,复杂度大概在 \(O(n^3)\) 左右
- 首先可以大致确定一个小阶段是进行的轮数,但并不一定用轮数作为一个dp的一个纬度,可能是其他与轮数相关的东西
- 询问剩余 \(0\) 人,多少种不同初始生命值的方案数,这个集合可以由 剩余 \(0\) 人,遭受了 \(j\) 次攻击的集合组成 (\(0\leq j\leq x\))
- 状态表示:
dp[i][j]
当前轮剩下 \(i\) 个人,已经遭受 \(j\) 次攻击的情况 - 考虑如何转移,遍历下一轮剩余的人数为 \(k\),则被更新的状态是
dp[k][min(j + i - 1, x)]
- 状态转移:
- \(dp[k][min(x, j + i - 1)] = dp[k][min(x, j + i - 1)] + dp[i][j] * C_i^{i - k} * (min(x, j + i - 1) - j) ^ {i-k}\)
- \(i\) 个人里面选 \(i-k\) 个人死亡,死亡的 \(i-k\) 个人里的生命值组成情况可以是 \([1, min(x, j + i - 1) - 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 ( \(\color{#F9F}{2000}\) )
题意
现有 \(n\) 名学生,老师希望学生 \(i\) 阅读消息 \(m_i\) 。
老师可以发布 \(t\) 条消息,但每个学生只会随机选择其中 \(k_i\) 条进行阅读,发布信息数量大于 \(k_i\) 条时,随机选择 \(k_i\) 条信息阅读。
请给出一种发布信息的方案,使得在期望条件下,有尽可能多的学生 \(i\) 阅读了对应的信息 \(m_i\) 。
数据范围
\(1\leq n\leq 2*10^5\)
\(1\leq k_i \leq 20\)
\(1\leq t \leq 2*10^5\)
思路
- 从数据范围入手,\(k_i\) 最多只有20个,尝试一下暴力,进行分类讨论
- 单独看每个信件被阅读到的期望贡献,答案就是 \(t\) 个信件期望相加,对于一个学生阅读信件数 \(k_i\) 来说,被期望阅读该信件,则该信件期望增加 \(\frac{min(k_i, t)}{t}\)
- 在不考虑 \(t\) 的情况下,遍历每个学生,可以获得所有信件的期望,如果 \(t\) 能够确定,那么我们只需要取期望前 \(t\) 大的信件就是最优的。
- 如果 \(t > 20\),由上可知对于每个信件的期望分母一直变大,由于 \(k_i<20\),所以随着 \(t\) 变大,最后期望变小,所以毫无意义。
- 现在考虑 \(t \leq 20\),由于随着 \(t\) 变大,尽管看似单个信件的期望在变小,信件数量增加,总期望变化不确定,而由于 \(t\) 最多只有 \(20\),我们就直接暴力枚举 \(t\) 的情况,取最优解即可。
- 时间复杂度: \(O(20 * 2e5 * log(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 ( \(\color{#F9F}{1900}\) )
题意
给定长度 \(n\) 的数组 \(a\),其中 \(0\leq a_i \leq n\) 询问合法子序列的个数,合法子序列的定义,对于这个序列的每一个前缀,最后一个数和前缀的 \(MEX\) 差值的绝对值不大于 \(1\)。
答案 \(\mod 998244353\) 。
数据范围
\(1\leq n \leq 5*10^5\)
\(0\leq a_i \leq n\)
思路
- 多画几次可以感觉出大概是个 状态机DP
- 合法序列的状态只有以下几种:
0 1 1 2 2 3 4 5
0 2 2 0 0 2 0 2
- 由第一种变为第二种
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]
,第二种情况选或不选 + 将第一种情况变成第二种情况
- 答案统计 \(\Sigma_{i = 0}^n f[i][0] + f[i][1] \mod 998244353\)
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 ( \(\color{#F9F}{2000}\) )
题意
有 \(3\) 种货币,每个价值 \(1,2,3\) 元,给出 \(n\) 个商品的价值,每个价值为 \(a_i\) ,询问最少的货币数能够准确买下所有商品
思路
- 此题毒瘤分类讨论,由于最大面值为 \(3\) ,所以 \(3\) 元货币尽可能多,贪心地对最贵商品进行讨论,同时价值相同商品无意义排序去重。
- 设最大商品价值为
x
,对 \(x \mod 3\) 模数讨论。 - 模数为 \(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 ( \(\color{#F9F}{1900}\) )
题意
给一个数组初始为空,进行 \(q\) 次操作,一个是向数组末尾插数,一个是将数组中所有的 x
替换乘 y
数据范围
\(1\leq q \leq 5 * 10^5\)
\(1\leq x,y \leq 5 * 10^5\)
思路
- 模拟一下,发现每次将 \(x\) 向 \(y\) 连边,统计最终值就是看 \(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 ( \(\color{#FA8}{2200}\) )
题意
有 \(n\) 个人做 \(m\) 道题,知道每个人做题的对错情况。每个人有一个预期得分 \(x_i\) 和实际得分 \(r_i\) 。\(x_i\) 给定,而 \(r_i\) 是该人做对题目的分数之和。定义 “惊喜度” 为 每个人的 \(∣x_i−r_i∣\) 之和。
请你构造出一个每道题的分数方案,使得:
- 分数是一个 \(1∼m\) 的排列。
- 在所有的方案中,该方案的 “惊喜度” 最大。
数据范围
\(1\leq n \leq 10\)
\(1\leq m \leq 10^4\)
\(0\leq x_i \leq \frac{m(m+1)}{2}\)
思路
- 观察数据范围,\(n\leq 10\),可以二进制枚举每个人的得分绝对值情况,拆绝对值
- 答案为 \(\Sigma_{i=1}^n (c[i]*x[i] - c[i] * r[i])\)
- 前半部分固定,想要答案最大,则后半部分的减数最小,那么对应答对次数小的题目应该获得更高的分数。
- 枚举不同
c[i]
情况,然后计算每个题目的答对次数(有负数),根据次数排序,从小到大赋值,更新答案即可。 - 这题时间复杂度比较玄学。按道理就是 \(O(2^n*m + 2^n*m*logm)\),却跑的挺快。
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 ( \(\color{#FA8}{2200}\) )
题意
给了一张无向完全图,点数 \(n\) 和边权值范围 \(k\),求合法图的数量 \(\mod 998244353\),合法图的定义是,与 \(1\) 号点连接的所有边边权和是 \(MST\) 的大小。
数据范围
\(2\leq n \leq 250\)
\(1\leq k \leq 250\)
思路
- 不难看出,主要核心是 1 号点,并且 1 号点连接的边的边权一定是最小的 \(n-1\) 个,否则不成立和为 \(MST\) ,很容易证明
- 由数据范围看出,应该是个 DP 题目,然后就开始玄学,可知非中心边的边权在
[中心边边权最大值,k]
范围内。 - DP阶段:加入图中的点数,方案数来源不同边的权值分配不同
- 状态表示: 根据最终答案是,\(n\) 个点,边权最大值为 \(k\) 的方案,状态定义为
dp[i][j]
, 选了 \(i\) 个点,最大值为 \(j\) 方案数 - 考虑如何转移,每加入一个新点,会产生
i - 1
条边,加入i-z
个新点,其中新点定义不同对答案有贡献,贡献为 \(C_{i-1}^{z-1}\) , 会产生(z-1) * (i-z) + (i-z) * (i-z-1) / 2
条新边。 - 第二维更新,从小到大遍历,
j
由j-1
转移,这样新产生的边不管是中心边还是非中心边,范围都在 \([j, k]\) 之间 - 状态转移: \(dp[i][j] = 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}\),答案为
dp[n][k]
- 时间复杂度:\(O(n^2k)\)
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 ( \(\color{#FA8}{2100}\) )
题意
给一颗节点数为 \(2^n - 1\) 满二叉树,点权为小写字母,根为 \(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 ( \(\color{#F9F}{1900}\) )
题意
D1的加强版,赛中过了,这里就不再单独将D1列出来。D1题意是输入一个数组 \(a(-1e6≤a_i≤1e6),\)n$ 个元素 \((4≤n≤40,n为偶数)\) 求出一个最大的整数 \(k\),对每个元素减去若干个 \(k\) 之后,\(a\)中所有元素相同。
此题(D2)的题意与D1大致相同,但求一个最大的 \(k\),使得数组中至少一半的元素能够相同
思路
- 两道题答案所包含的元素组成的子数组中,设最小值为 \(v\),不难发现,其他元素与\(v\)的差值(设为\(d\) )一定都对\(\%k\) 同余。(
应该这么表示吧?) - 由上一点可知,\(k\) 一定是所有 \(d\) 的最大公约数。
- D1可以求出最小元素 \(v\),再求其他元素相应的 \(d\),D2需要观察数据范围,采用暴力枚举所有元素为 \(v\),再筛选出其他满足条件的 \(d≥0\),存取所有 \(d\) 的约数,找出大于等于 \(\frac{n}{2}\) 的最大的那个。
自己想法
所有 \(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 ( \(\color{#FA8}{2100}\) )
题意
给了长度为 \(n\) 的字符串,由 '+' 和 '-' 组成,可以将字符串中两个 '+' 换成 1 个 '-',询问合法子串个数,合法子串定义,子串内 '+' 数量和 '-' 数量相同。
数据范围
\(1\leq n\leq 2*10^5\)
思路
- F1 \(n\) 只有 3000,是一个暴力,F2考虑优化。
- 我们只关心区间中 '-' 个数与 '+' 个数的差值,自然 '+' 个数比 '-' 小,多模拟一下发现一个简单结论:区间中两数差值 \(\mod 3 =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 ( \(\color{#AAF}{1800}\) )
题意
给了点数为 \(n\) 的树,和 \(k\) 个必须到达的点,出发的起点 \(x\) 和 到达终点 \(y\),边权为 \(1\) ,询问从起点经过指定的
\(k\) 个点,最后到达 \(y\) 的最短路径花费, \(k\) 个点的访问顺序可以任意。
数据范围
\(1\leq k \leq n \leq 2 * 10^5\)
思路
- 贪心地想,对于所有任务点必须到,然后要返回,那么起点到这些点的路径和至少为2倍到达这些点的花费。
- 问题转化为,经过 \(k\) 个点的"最小生成树",当然这里不需要最小生成树算法,只需要知道我们不需要经过的点数即可,剩余点数为 \(x\),路径长度为 \(x-1\) 。
- 那么现在访问了所有点,要更贪心的想,从 \(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 ( \(\color{#FB5}{2300}\) )
题意
给了一个长度 \(n\) 的数组 \(a\) ,和保证为 \(m\), 每次只能将 \(a_i\) 减 1, \(a_{i-1}\; or\; a_{i+1}\) 加 1,询问最小的操作次数使 \(a\) 非严格递减。
数据范围
\(1\leq n,m \leq 250\)
思路
- 毫无疑问,一道dp题,根据数据范围,大概可以容忍 \(O(n^3)\) 级别的时空复杂度。
- 在设计状态前,务必需要一个结论:
- 对于这样的相邻移动元素的操作而言,设操作后的结果为 \(b\) 数组,总操作次数等于 \(\Sigma_{i=1}^n(|S_b[i] - S_a[i]|)\) ,对应前缀和差的绝对值的和
- 首先dp的阶段是长度 \(i\),由于数组和固定,考虑加入一维当前数组的和 \(j\) ,由于要满足递增或者递减,加入一维记录最后一个数 \(k\) ,由于需要对比大小,这个数其实是一个最值。
- 那么状态已经设计出来了:
dp[i][j][k]
表示数组前 \(i\) 位,和为 \(j\) ,最后一位最值为 \(k\) 的最小操作次数。 - 考虑如何转移方便,如果按照题意原本描述来看,转移方程是遍历前一个数 \(x,k\leq x\)
dp[i][j][k] = min(dp[i][j][k], dp[i - 1][j - k][x] + abs(j - s[i]))
,观察发现可以使用最小后缀来优化 \(x\) 的遍历,此时预处理最小后缀复杂度为 \(O(n^3)\) - 而考虑反着做,即将数组翻转后,考虑将数组构造成非严格递增。那么 \(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)));
}
}
}
- 时间复杂度:将 \(n\) 和 \(m\) 同级,\(O(n^3)\) ,翻转数组的方法还没有想到如何优化成 \(O(n^2logn)\) 。
- 如果尝试采用记录后缀最小来优化,发现对于下标 \(i\) 上每个数不能超过
m / i
,否则它一定比前面的某个数大(平均值),加入循环条件中可以做到 \(O(n^2logn)\)。
- 如果尝试采用记录后缀最小来优化,发现对于下标 \(i\) 上每个数不能超过
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 ( \(\color{#CCC}{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 ( \(\color{#8DB}{1500}\) )
双指针, 贪心, 排序, 或者 STL
题意
给了 \(n\) 个货品, \(n\) 是偶数, 每个货品有价值 \(a_i\) , 将货品两两配对, 两两配对和总价值为 \(w_i\), 给定 \(k\), 求 \(\Sigma \lfloor \frac{w_i}{k} \rfloor\) 最大值
数据范围
\(2\leq n \leq 2*10^5\)
\(1\leq k \leq 1000\)
\(0\leq a_i \leq 10^9\)
思路
- 观察数据范围 , \(k\leq 1000\).
- 由于答案为重量之和向下取整, 对 \(a_i\) 来说, 无论怎样组合, \(a_i / k\) 的贡献是确定的.
- 这样我们只用关心 \(a_i \% k\) 的贡献, 贪心 地来想, 对于 \(x=a_i\% k\) , 需要找到满足最小余数 \(\geq k-x\)
- 可以用两种方式实现
- 一种是显然的双指针 (
呵呵我写挂了, 很离谱, 有时间来补这个解法) - 还有一种是 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 ( \(\color{#AAF}{1700}\) )
置换, 环, 字符串最小循环节, LCM
题意
给定一个长度为 \(n\) 的字符串 \(s\), 和一个排列 \(P\) , 每次操作让 \(s_i = s_{P_i}\) , 询问至少经过多少次操作能让字符串与初始状态相同
数据范围
\(1\leq n \leq 200\)
\(1\leq p_i \leq n\)
思路
- 草稿纸上画几遍, 发现可能与置换中所形成的各个环的长度有关系, 这个关系是各长度的 \(LCM\)
- 答案其实是与每个环经过多少次变换能变成原来的字符串, 由于是字符串关系, 这个多少次变换, 其实就是每个环上对应字符串的最小循环节.
- 所以答案就是各个环最小循环节长度的 \(LCM\)
- 如何求最小循环节? 此题可以暴力枚举循环节长度判断是否合法, 数据规模较大可以使用 KMP 求字符串最小循环节, 得到 \(ne\) 数组判断
if(len % (len - ne[len]) == 0)
- 此处给出 KMP 的代码 (依然有些参考 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;
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 ( \(\color{#F9F}{2000}\) )
STL, 二分, 思维
题意
给了 \(n\) 个点, 每个点有值为 \(a_i\) , 从左往右形成序列, 对于序列上下标 \(i\) 位置, 值为 \(min\{a_j\}, (j\leq i)\)
有 \(m\) 次操作, 输入 \(k, d\) , 每次操作对 a[k] -= d
, 询问每次操作完之后, 序列中有多少个不同的数 ?
思路
- 容易挖掘题目性质, 对于一个数 \(a_i\) , 如果 \(\exist j < i, a_j\leq a_i\) , 那么 \(a_i\) 不会对答案产生贡献
- 由于答案是求不同的数个数 , 我们可以联想到
map, set
这一类 STL 容器来解决, 那么问题是如何实现. - 参照第一点, 我们尝试用
map<int,int>
容器来实现, 第一关键字为下标, 第二关键字为对应值 - 每次输入或操作对 map 中插入
i, a[i]
, 若前面迭代器元素值小于等于 \(a_i\) 则删除现在插入的迭代器, 若没有删除, 检查后面的迭代器是否满足要求(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