2021.04.30
训练地址
B. The Number of Pairs
- 令 u = g c d ( a , b ) u = gcd(a, b) u=gcd(a,b),设 l c m ( a , b ) = k ∗ u lcm(a, b) = k * u lcm(a,b)=k∗u,则原式可写为 u ∗ ( k ∗ c − d ) = x u * (k * c - d) = x u∗(k∗c−d)=x,那么有整数解,必须要满足 u ∣ x u | x u∣x. 则我们只需要枚举 x x x 的约数,这样子,就可以求出 k k k 了。
- k = x / u + d c k = \frac{x/u+d}{c} k=cx/u+d. 那么,我们确定了 g c d gcd gcd 与 l c m lcm lcm,这样子的话, ( a , b ) (a,b) (a,b) 的组合数就是 2 l c m / g c d 的质因数个数 2^{lcm / gcd的质因数个数} 2lcm/gcd的质因数个数.
- 那么我们先晒出来每个数质因数的个数即可,这个地方还挺容易错的,就是在计算 s t [ i ∗ p r i m e [ j ] ] st[i * prime[j]] st[i∗prime[j]] 的地方的时候,要分 i % p r i m e [ j ] = = 0 i \% prime[j] == 0 i%prime[j]==0 讨论。因为每一个质数只能算入一次。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 20000010;
int prime[N], cnt;
int st[N];
ll pw[100] = {1};
void sieve(int n)
{
for(int i = 2; i <= n; i++){
if(!st[i]){
prime[cnt++] = i;
st[i] = 1;
}
for(int j = 0; prime[j] <= n / i; j++){
if(i % prime[j] == 0) st[i * prime[j]] = st[i];
else st[i * prime[j]] = st[i] + 1;
if(i % prime[j] == 0) break;
}
}
for(int i = 1; i < 30; i++) pw[i] = pw[i - 1] * 2;
}
vector<int> divisor(ll n)
{
vector<int> res;
for(int i = 1; i <= n / i; i++){
if(n % i == 0){
res.push_back(i);
if(n / i != i) res.push_back(n / i);
}
}
return res;
}
int main()
{
sieve(N - 1);
int T;
scanf("%d", &T);
while(T--){
ll ans = 0;
ll a, b, c;
scanf("%lld%lld%lld", &a, &b, &c);
vector<int> res = divisor(c);
for(auto p : res){
ll tmp = c / p + b;
if(tmp % a) continue;
ll k = tmp / a;
ans += pw[st[k]];
}
printf("%lld\n", ans);
}
return 0;
}
C. Chaotic Merge
- 题意:
- 题解:
- 其实这个题相当于有很多dp的起点(拓扑图的起点),那么可以边递推边加入新的起点.
- 而底下不断加入
d
p
x
i
,
d
p
y
j
dpx_i, dpy_j
dpxi,dpyj,就是不断加入新的起点的过程. 比如
d
p
(
i
,
j
,
0
)
+
=
d
p
y
j
dp(i, j, 0) += dpy_j
dp(i,j,0)+=dpyj 就是表示加入了一个一串 y 中的连续字符串加上一个
x
i
x_i
xi 中的字符串作为一个起点.
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
typedef long long ll;
const ll mod = 998244353;
char x[N], y[N];
ll f[N][N][2], fx[N], fy[N];
int main()
{
scanf("%s%s", x + 1, y + 1);
int n = strlen(x + 1);
int m = strlen(y + 1);
for(int i = 1; i <= n; i++){
if(x[i] != x[i - 1]) fx[i] = fx[i - 1] + 1;
else fx[i] = 1;
}
for(int i = 1; i <= m; i++){
if(y[i] != y[i - 1]) fy[i] = fy[i - 1] + 1;
else fy[i] = 1;
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
if(x[i] != y[j]){
f[i][j][0] = (f[i][j][0] + fy[j]) % mod;
f[i][j][1] = (f[i][j][1] + fx[i]) % mod;
}
if(x[i] != x[i - 1]){
f[i][j][0] = (f[i][j][0] + f[i - 1][j][0]) % mod;
}
if(x[i] != y[j]){
f[i][j][0] = (f[i][j][0] + f[i - 1][j][1]) % mod;
f[i][j][1] = (f[i][j][1] + f[i][j - 1][0]) % mod;
}
if(y[j] != y[j - 1]){
f[i][j][1] = (f[i][j][1] + f[i][j - 1][1]) % mod;
}
}
}
ll ans = 0;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
for(int k = 0; k <= 1; k++){
ans = (ans + f[i][j][k]) % mod;
}
}
}
printf("%lld\n", ans);
return 0;
}
D. Diameter Cuts
- 给一棵树,让你删掉一些边,使得生成的森林的每棵树的直径都不超过k. 问删除的边组成的集合,有多少这样的集合。
- f ( u , l e n ) f(u, len) f(u,len) 表示以 u 为根节点的子树,那么我们可以两两子树遍历, f ( u , l e n ) + = f ( v 1 , i ) + f ( v 2 , j ) , i + 1 + j + 1 ≤ k . f(u, len) += f(v_1, i) + f(v_2, j),\quad \ i+1 + j + 1 \le k. f(u,len)+=f(v1,i)+f(v2,j), i+1+j+1≤k.
- 但其实我们可以依次枚举子结点,看它对答案的贡献,这也是一个常见技巧了,具体可以看代码. 而且我们知道, f ( v , j ) f(v, j) f(v,j) 中的 j j j 不可能超过子树的深度.
#include<bits/stdc++.h>
using namespace std;
const int N = 5010, M = 2 * N;
typedef long long ll;
const ll mod = 998244353;
int n, k;
ll f[N][N];
int h[N], e[M], ne[M], idx;
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
int dfs(int u, int fa)
{
int height = 0;
f[u][0] = 1;
for(int i = h[u]; i != -1; i = ne[i]){
int v = e[i];
if(v == fa) continue;
int nh = dfs(v, u);
vector<int> tmp(max(height, nh + 1) + 1);
for(int i = 0; i <= height; i++){
for(int j = 0; j <= nh; j++){
if(i + j + 1 <= k){
tmp[max(i, j + 1)] = (tmp[max(i, j + 1)] + f[u][i] * f[v][j] % mod) % mod;
}
if(i <= k){
tmp[i] = (tmp[i] + f[u][i] * f[v][j]) % mod;
}
}
}
height = max(height, nh + 1);
for(int i = 0; i <= height; i++){
f[u][i] = tmp[i];
}
}
return height;
}
int main()
{
memset(h, -1, sizeof h);
scanf("%d%d", &n, &k);
for(int i = 1; i < n; i++){
int a, b;
scanf("%d%d", &a, &b);
add(a, b), add(b, a);
}
dfs(1, -1);
ll ans = 0;
for(int i = 0; i <= k; i++){
ans = (ans + f[1][i]) % mod;
}
printf("%lld\n", ans);
return 0;
}
G. Genius
- 题意:有n个题,从前往后的复杂度 c i c_i ci 为 2 i 2^i 2i,我们如果刚解决问题 j j j,想要解决问题 i i i,就要满足 I Q < ∣ c i − c j ∣ IQ<|c_i-c_j| IQ<∣ci−cj∣,解完 i i i 后, I Q IQ IQ 就会变为 ∣ c i − c j ∣ |c_i-c_j| ∣ci−cj∣,并得到 ∣ s i − s j ∣ |s_i-s_j| ∣si−sj∣ 分,刚开始 I Q IQ IQ 为 0 0 0,我们可以先解任意一个题,一个题可以解决多次,我们要得出最多可以有多少分.
- 我们可以证明,任意两点间的 ∣ c i − c j ∣ |c_i - c_j| ∣ci−cj∣ 的数值是不同的. 假设 i > j i > j i>j,因为 ∣ 2 i − 2 j ∣ |2^i - 2^j| ∣2i−2j∣ 是从 j j j 到 i − 1 i - 1 i−1 位全是1,其他全是0.
- 我们还发现,如果我们随意走出一条路径,那么我们只需要调整一下走的顺序,就是一条合法路径。因此我们只需要枚举 ( i , j ) (i, j) (i,j) 即可.
- 最后,我们定义 f i f_i fi 表示在第 i 个点结束做题时的答案,我们第一维枚举结束的顶点,第二维倒着枚举,从 i − 1 i - 1 i−1 到 1,这样子一定可以保证最后走的那条路是最大值.
#include<bits/stdc++.h>
using namespace std;
const int N = 5010;
typedef long long ll;
ll f[N], tag[N], s[N];
int main()
{
int T;
scanf("%d", &T);
while(T--){
int n;
scanf("%d", &n);
memset(f, 0, sizeof f);
for(int i = 1; i <= n; i++){
scanf("%lld", &tag[i]);
}
for(int i = 1; i <= n; i++){
scanf("%lld", &s[i]);
}
for(int i = 1; i <= n; i++){
for(int j = i - 1; j >= 1; j--){
if(tag[i] == tag[j]) continue;
ll p = abs(s[i] - s[j]), fi = f[i], fj = f[j];
f[i] = max(f[i], fj + p), f[j] = max(f[j], fi + p);
}
}
printf("%lld\n", *max_element(f + 1, f + n + 1));
}
return 0;
}
H. Square-free division (easy version)
- 这个题想复杂了。我们把所有的数字的质因数的幂都模2,这样子的话,我们从前往后遍离数组,并用一个map记录之前哪些数字出现过。
- 如果新加进来的改变的数字并不存在于 map 中,说明当前数字加入当前区间不会引起冲突,于是把这个数字加入当前的区间之中。
- 如果之前这个数出新过,那么就在这个数前面画一条分界线,把map清空,把这个数加到map里面,然后答案加1.
#include<bits/stdc++.h>
using namespace std;
const int N = 10000010;
int prime[N], cnt, st[N];
void sieve(int n)
{
for(int i = 2; i <= n; i++){
if(!st[i]) prime[cnt++] = st[i] = i;
for(int j = 0; prime[j] <= n / i; j++){
st[i * prime[j]] = prime[j];
if(i % prime[j] == 0) break;
}
}
}
int change(int x)
{
int res = 1;
while(x > 1){
int u = st[x];
int cnt = 0;
while(x % u == 0){
cnt++;
x /= u;
}
if(cnt & 1) res *= u;
}
return res;
}
int main()
{
sieve(N - 1);
int T;
scanf("%d", &T);
while(T--){
set<int> S;
int n, k;
scanf("%d%d", &n, &k);
int ans = 1, tmp;
for(int i = 1; i <= n; i++){
scanf("%d", &tmp);
tmp = change(tmp);
if(S.count(tmp)){
ans++;
S.clear();
}
S.insert(tmp);
}
printf("%d\n", ans);
}
return 0;
}
I. Square-free division (hard version)
- 给一个序列 n ≤ 2 e 5 n \le 2e5 n≤2e5,问最少分割成几个连续字段,使得每个连续字段中不存在两个数的积是完全平方数. 可以改变中间的某个数字至多 k ( k ≤ 20 ) k(k \le 20) k(k≤20) 次,改为任意的数字即可.
- 我们先处理出 L e f t ( i , j ) Left(i,j) Left(i,j) 表示从 i i i 开始,可以删掉 k k k 个 j j j 个数字,满足 [ l , i ] [l,i] [l,i] 中间是合法连续子序列的最往左的左区间端点是多少. 这个可以用双指针在 O ( n k ) O(nk) O(nk) 的时间内解决.
- 然后令
f
(
i
,
j
)
f(i,j)
f(i,j) 表示前
i
i
i 个数字,改变
j
j
j 个数字的最少划分数是多少. 这个我们可以枚举
x
∈
[
0
,
j
]
x \in [0,j]
x∈[0,j],
l
=
L
e
f
t
(
i
,
j
)
l = Left(i,j)
l=Left(i,j),可以分为从两块,及
[
1
,
l
−
1
]
[1, l - 1]
[1,l−1] 的最优划分加上
[
l
,
i
]
[l,i]
[l,i] 作为一个完整连续子段,并且前者改变
j
−
x
j - x
j−x 次,后者改变
x
x
x 次.
f ( i , j ) = min { f ( i , j ) , f ( l − 1 , j ) + 1 } f(i,j) = \min\{f(i,j),f(l-1,j) + 1\} f(i,j)=min{f(i,j),f(l−1,j)+1} - 初始化的时候, f f f初始化为正无穷, f ( 0 , 0 ) f(0,0) f(0,0) 初始化为1即可.
#include<bits/stdc++.h>
using namespace std;
const int N = 200010, M = 10000010;
int prime[M], st[M], cnt;
void sieve(int n)
{
for(int i = 2; i <= n; i++){
if(!st[i]) prime[cnt++] = st[i] = i;
for(int j = 0; prime[j] <= n / i; j++){
st[i * prime[j]] = prime[j];
if(i % prime[j] == 0) break;
}
}
}
int change(int x)
{
vector<int> res;
while(x > 1){
int u = st[x];
int cnt = 0;
while(x % u == 0){
x /= u;
cnt++;
}
if(cnt & 1) res.push_back(u);
}
int ans = 1;
for(auto p : res){
ans = ans * p;
}
return ans;
}
int a[N], n, k, Left[N][30], f[N][30];
unordered_map<int, int> Map(N);
int main()
{
sieve(M - 1);
int T;
scanf("%d", &T);
while(T--){
scanf("%d%d", &n, &k);
for(int i = 1; i <= n; i++){
int x;
scanf("%d", &x);
a[i] = change(x);
}
for(int i = 1; i <= n; i++){
for(int j = 0; j <= k; j++){
Left[i][j] = 0;
f[i][j] = 1e9;
}
}
f[0][0] = 0;
for(int lim = 0; lim <= k; lim++){
Map.clear();
int cnt = 0;
for(int i = 1, j = 1; i <= n; i++){
Map[a[i]]++;
if(Map[a[i]] > 1) cnt++;
while(i >= j && cnt > lim){
if(Map[a[j]] > 1){
cnt--;
}
Map[a[j]]--;
j++;
}
Left[i][lim] = j;
}
}
for(int i = 1; i <= n; i++){
for(int j = 0; j <= k; j++){
for(int x = 0; x <= j; x++){
int l = Left[i][x];
f[i][j] = min(f[i][j], f[l - 1][j - x] + 1);
}
}
}
int ans = 1e9;
for(int j = 0; j <= k; j++){
ans = min(ans, f[n][j]);
}
printf("%d\n", ans);
}
return 0;
}
Codeforces Global Round 14
训练地址
E. Phoenix and Computers
- 给了一排电脑 n ≤ 400 n \le 400 n≤400,最开始全是关闭状态,可以一个一个打开它. 当打开了 i − 1 i - 1 i−1 和 i + 1 i + 1 i+1 的电脑的时候,第 i i i 个电脑也会自动打开.
- 令
f
(
l
e
n
,
c
n
t
)
f(len, cnt)
f(len,cnt) 表示点亮了前
l
e
n
−
1
len - 1
len−1 个电脑,手动打开了
c
n
t
cnt
cnt 个,假如后面又打开了
k
k
k 个连续段,那么状态转移方程是
f ( l e n + k + 1 , c n t + k ) + = f ( l e n , c n t ) ∗ C l e n + c n t c n t ∗ 2 k − 1 . f(len + k + 1, cnt + k) += f(len, cnt) * C_{len + cnt}^{cnt} * 2 ^{k - 1}. f(len+k+1,cnt+k)+=f(len,cnt)∗Clen+cntcnt∗2k−1. - 一个需要注意的地方是,dp 的起点,就是 f ( 0 , 0 ) = 1 f(0, 0) = 1 f(0,0)=1. 因为从 ( 0 , 0 ) (0, 0) (0,0) 出发可以到达任何状态.
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 510;
ll n, mod;
ll f[N][N], pw[N], fact[N], infact[N];
ll mod_pow(ll x, ll n)
{
ll res = 1;
while(n){
if(n & 1) res = res * x % mod;
x = x * x % mod;
n >>= 1;
}
return res;
}
ll C(ll a, ll b)
{
return fact[a] * infact[a - b] % mod * infact[b] % mod;
}
int main()
{
scanf("%lld%lld", &n, &mod);
fact[0] = infact[0] = 1;
for(ll i = 1; i < N; i++){
fact[i] = i * fact[i - 1] % mod;
infact[i] = infact[i - 1] * mod_pow(i, mod - 2) % mod;
}
pw[0] = 1;
for(int i = 1; i < N; i++){
pw[i] = pw[i - 1] * 2 % mod;
}
f[0][0] = 1;
for(int i = 0; i <= n + 1; i++){
for(int j = 0; j <= i; j++){
for(int k = 1; k <= n + 1; k++){
if(i + k + 1 > n + 1) break;
f[i + k + 1][j + k] = (f[i + k + 1][j + k] + f[i][j] * C(j + k, k) % mod * pw[k - 1] % mod) % mod;
}
}
}
ll ans = 0;
for(int i = 1; i <= n; i++){
ans = (ans + f[n + 1][i]) % mod;
}
printf("%lld\n", ans);
return 0;
}
2021.05.03
训练地址
B. Two chandeliers
G. Garden of the Sun
- 题意:给一个矩阵,上面有些格子是 ‘X’,有些格子是 ‘.’,并且保证所有的 ‘X’ 都是非八连通的。现在要加一些 ‘X’ 使得他们成为四联通,并且没有环
- 做法:充分利用非八连通的性质,从第一行开始,每隔两行,就把这一行全部涂成 ‘X’,然后中间那两行,如果出现了 ‘X’,就把上下两个格子都填成 ‘X’,然后 break. 如果没有出现过 ‘X’,就把第一列的两个格子填成 ‘X’.
- 但是,有一个特殊情况,就是 n % 3 == 0 的情况,这个最后两行可能会出现不连通的情况。那么就把最后两行只要出现 ‘X’,就把上下两个格子填成 ‘X’.
#include<bits/stdc++.h>
using namespace std;
const int N = 510;
char s[N][N];
int main()
{
int T;
scanf("%d", &T);
while(T--){
int n, m;
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++){
scanf("%s", s[i] + 1);
}
for(int i = 1; i <= n; i += 3){
if(i % 3 == 1){
for(int j = 1; j <= m; j++) s[i][j] = 'X';
}
if(i + 2 <= n){
bool flag = true;
//printf("***\n");
for(int j = 1; j <= m && flag; j++){
if(s[i + 1][j] == 'X' || s[i + 2][j] == 'X'){
s[i + 1][j] = s[i + 2][j] = 'X';
flag = false;
//printf("*** %d %d\n", i, j);
}
}
if(flag) s[i + 1][1] = s[i + 2][1] = 'X';
}
}
if(n % 3 == 0){
for(int j = 1; j <= m; j++){
if(s[n - 1][j] == 'X' || s[n][j] == 'X'){
s[n - 1][j] = s[n][j] = 'X';
}
}
}
for(int i = 1; i <= n; i++) s[i][m + 1] = 0;
for(int i = 1; i <= n; i++){
printf("%s\n", s[i] + 1);
}
}
return 0;
}