比赛链接:
https://vjudge.net/contest/510913
A - Arithmetic Subsequence
题意:
长为 \(n\) 的序列,对它重新排序,使得对于 \(1 <= i < j < k <= n\),\(a_i, a_j, a_k\) 不构成等差序列,若能,输出 YES 以及重构的序列,若不能输出 NO。
思路:
如果某个数出现了三次及以上,则不可能构成,剩余的都可以。
对于三个数如果构成了等差序列,即 \(a_i + 2d = a_j + d = a_k\),那么 \(a_i + a_k = 2 * a_j\),可以知道 \(a_i\) 和 \(a_k\) 的奇偶性是相同的。
为了避免这种情况的出现,对于一个序列,将偶数放到序列左侧,将奇数放到序列右侧。
接下来考虑三个奇数的情况,\(2x + 1, 2y + 1, 2z + 1\),如果构成等差序列 \(2x + 1 + 2z + 1 = 2 * (2y + 1)\),可以发现三个数都整除 2,是不影响是否构成等差的。
三个偶数的情况,\(2x, 2y, 2z\),跟奇数同样道理,形成的偶数和奇数序列,都整除 2,然后继续分治操作即可。
代码:
#include <bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 5e3 + 10;
LL n;
vector < array<LL, 2> > a(N);
void merge (LL L, LL R){
if (L + 1 >= R) return;
vector < array<LL, 2> > b;
LL mid = L;
for (int i = L; i <= R; i ++ )
if (a[i][0] % 2 == 0){
b.push_back(a[i]);
mid ++ ;
}
for (int i = L; i <= R; i ++ )
if (a[i][0] & 1)
b.push_back(a[i]);
for (int i = L; i <= R; i ++ ){
b[i - L][0] >>= 1;
a[i] = b[i - L];
}
merge(L, mid - 1);
merge(mid, R);
}
void solve(){
cin >> n;
map <LL, int> cnt;
for (int i = 0; i < n; i ++ ){
cin >> a[i][1];
cnt[a[i][1]] ++ ;
a[i][0] = a[i][1];
}
for (auto t : cnt){
if (t.second >= 3){
cout << "NO\n";
return;
}
}
cout << "YES\n";
merge(0, n - 1);
for (int i = 0; i < n; i ++ )
cout << a[i][1] << " \n"[i == n - 1];
}
int main(){
ios::sync_with_stdio(false);cin.tie(0);
LL T = 1;
cin >> T;
while(T -- ){
solve();
}
return 0;
}
C - Fast Bubble Sort
题意:
长为 \(n\) 的排列 \(p\),\(q\) 次询问,每次询问区间 \([l, r]\),将这个区间进行一次冒泡排序之后变成序列 \(a\)。
每次可以选择一个 \([l, r]\) 中的一个区间对其中的元素进行循环移动,即将所有元素依次向前移动一个,第一个移到最后一个,或者反一下。问最少几次操作可以让这个区间形成的序列等于 \(a\)。
思路:
一次冒泡排序其实就是将每一个元素移动到它后面第一个比它大的元素的位置的前一个。可以先用单调栈从后往前将每个元素后面第一个比它大的元素记录下来。
在询问时,如果就采用朴素的方式,每次跳到自己后面第一个比自己大的位置然后输出结果,面对已经拍好序的序列,会超时。
每次跳转到另一个位置,如果是在树上操作的话,其实就是倍增求 \(LCA\),所以考虑通过倍增去加快跳转的速度。
代码:
#include <bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 1e5 + 10;
LL n, q, p[N], R[N], fa[N][30], ans[N];
void solve(){
cin >> n >> q;
for (int i = 1; i <= n; i ++ )
cin >> p[i];
p[n + 1] = 1e9;
stack <LL> stk;
stk.push(n + 1);
for (int i = n; i >= 1; i -- ){
while(stk.size() && p[stk.top()] < p[i]){
stk.pop();
}
R[i] = stk.top();
stk.push(i);
}
for (int i = 0; i <= n + 1; i ++ )
for (int j = 0; j < 21; j ++ )
fa[i][j] = n + 1;
ans[n + 1] = 0;
for (int i = n; i >= 1; i -- ){
ans[i] = ans[R[i]];
if (R[i] > i + 1){
ans[i] ++ ;
}
fa[i][0] = R[i];
for (int j = 1; j <= 20; j ++ )
fa[i][j] = fa[fa[i][j - 1]][j - 1];
}
while(q -- ){
LL l, r;
cin >> l >> r;
LL L = l;
for (int j = 20; j >= 0; j -- )
if (fa[L][j] <= r)
L = fa[L][j];
LL res = ans[l] - ans[L];
if (L != r) res ++ ;
cout << res << "\n";
}
}
int main(){
ios::sync_with_stdio(false);cin.tie(0);
LL T = 1;
cin >> T;
while(T -- ){
solve();
}
return 0;
}
G - Matryoshka Doll
题意:
长为 \(n\) 的非递减序列,问将 \(n\) 个数分成 \(k\) 组,每组中的数排序之后,相邻的两个数之间差值要 >= \(r\) 的方案数。
思路:
定义 \(dp[i][j]\) 为将前 \(i\) 个数分成 \(j\) 组的方案。
对于新加入的第 \(i\) 个数 \(a[i]\),它可以作为一个新的组或者加入旧的一个组,由此得到转移方程 \(dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j] * (合法的组的数量)\)。
代码:
#include <bits/stdc++.h>
using namespace std;
#define LL long long
const int P = 998244353, N = 5e3 + 10;
LL a[N], dp[N][N];
void solve(){
LL n, k, r;
cin >> n >> k >> r;
for (int i = 1; i <= n; i ++ )
cin >> a[i];
dp[0][0] = 1;
for (int i = 1; i <= n; i ++ ){
int s = 0; //至少要有 s 组才能将第 i 个数加入到前面的一个组中
for (int j = i - 1; j > 0; j -- ){
if (a[j] + r > a[i]) s ++ ;
else break;
}
for (int j = 1; j <= i; j ++ )
dp[i][j] = (dp[i - 1][j - 1] + dp[i - 1][j] * max(0, j - s) % P) % P;
}
cout << dp[n][k] << "\n";
}
int main(){
ios::sync_with_stdio(false);cin.tie(0);
LL T = 1;
cin >> T;
while(T -- ){
solve();
}
return 0;
}
J - Sum Plus Product
题意:
\(n\) 个数,每次拿两个出来,设为 \(a, b\),将 \(a + b + a * b\) 放回去,问最后剩下的那个数的期望。
思路:
赛时思路:跟拿的顺序没有关系,直接模拟。
题解的思路:
代码:
#include <bits/stdc++.h>
using namespace std;
#define LL long long
const int P = 998244353;
void solve(){
LL n;
cin >> n;
vector <LL> a(n);
for (int i = 0; i < n; i ++ )
cin >> a[i];
if (n == 1) cout << a[0] << "\n";
else {
LL ans = (a[0] + a[1] + a[0] * a[1] % P) % P;
for (int i = 2; i < n; i ++ )
ans = (ans + a[i] + ans * a[i] % P) % P;
cout << ans << "\n";
}
}
int main(){
ios::sync_with_stdio(false);cin.tie(0);
LL T = 1;
cin >> T;
while(T -- ){
solve();
}
return 0;
}
H - Shortest Path in GCD Graph
题意:
在一个有 \(n\) 个点的完全图(每个点之间有边)中,第 \(i\) 个点和第 \(j\) 个点之间的距离为 \(gcd(i, j)\),\(q\) 次询问,每次询问 \(x\) 和 \(y\) 之间的最短路径长度以及最短路径的数量。
思路:
容易发现,最短路径长度只可能是 1 或者 2。
当 \(gcd(x, y) = 1\) 时,显然最短路径长度就是 1,且只有 1 条。
当 \(gcd(x, y) != 1\),那么 \(x\) 可以先到 1,然后去 \(y\),所以最短路径长度为 2。
最短路径的数量就是 1 到 \(n\) 中,所有与 \(x\) 和 \(y\) 互质的数的数量,这个可以通过容斥原理去求解。
先将 \(x\) 和 \(y\) 的所有质因数分解出来,存在 \(fac\) 数组中,所有质因数的倍数的点的路径都不是最短路径的,即找到 1 到 \(n\) 中不能被 \(fac\) 中元素整除的数。
同时,如果 \(gcd(x, y) = 2\),那么这也是一条最短路径,答案 + 1。
代码:
#include <bits/stdc++.h>
using namespace std;
#define LL int
const int N = 1e6 + 10, M = 1e7 + 10;
LL prime[N], cnt, f[M], n, q, ans, m;
bool st[M];
set <LL> s;
vector <LL> fac;
void sieve(int n){
st[1] = true;
for (int i = 2; i <= n; i ++ ){
if (!st[i]) prime[ ++ cnt] = i, f[i] = i;
for (int j = 1; j <= cnt && i * prime[j] <= n; j++){
st[i * prime[j]] = true;
f[i * prime[j]] = prime[j];
if (i % prime[j] == 0) break;
}
}
}
void resolve(LL x){
while(x > 1){
LL y = f[x];
s.insert(y);
while(x % y == 0){
x /= y;
}
}
}
void dfs(LL x, LL s, LL odd){
if (x == m){
ans += odd * (n / s);
return;
}
dfs(x + 1, s, odd);
if (s <= n / fac[x]) dfs(x + 1, s * fac[x], -odd);
}
int main(){
ios::sync_with_stdio(false);cin.tie(0);
cin >> n >> q;
sieve(n);
while(q -- ){
LL x, y;
cin >> x >> y;
if ( __gcd(x, y) == 1){
cout << "1 1\n";
}
else{
s.clear();
resolve(x);
resolve(y);
fac.clear();
for (auto t : s)
fac.push_back(t);
m = fac.size();
ans = 0;
dfs(0, 1, 1);
if ( __gcd(x, y) == 2) ans ++ ;
cout << "2 " << ans << "\n";
}
}
return 0;
}