文章目录
A. LCT
Problem
给定 n n n 个节点的有根树,共有 n − 1 n-1 n−1 次询问,每次询问给定一条边 ( a i , b i ) (a_i,b_i) (ai,bi),表示节点 a i a_i ai 是节点 b i b_i bi 的父节点,以及 c i c_i ci,问当前以 c i c_i ci 为根的树的高度是多少?
数据范围: 2 ≤ n ≤ 1 0 6 , 1 ≤ a i , b i , c i ≤ n , a i ≠ b i 2\le n\le10^6,1\le a_i,b_i,c_i\le n,a_i\neq b_i 2≤n≤106,1≤ai,bi,ci≤n,ai=bi
Solution
注意到每次给出的边其实是将两颗树合并成一棵树的操作,因为节点 b i b_i bi 一定是某棵树的根节点,而节点 a i a_i ai 可能是根节点也可能是树的其他节点。
树的高度是所有节点深度最大的点,因此只要维护好每个节点的深度,同时在两棵树合并的时候维护好最大的深度即可。
记为数组 d i d_i di 表示节点的深度,数组 h i h_i hi 表示树的高度。
节点的深度可以通过带权并查集维护。带权并查集通过路径压缩的方式能够以不错的复杂度维护好节点的深度。
树的高度则在合并的时候维护。合并后有两种情况,一种是高度没有改变,另一种是变大了,而变大后的值肯定和节点 a i a_i ai 的深度以及另一颗树的高度有关。
因此先通过并查集得到节点 a i a_i ai 所在树的根节点为 x x x,那么 h x = m a x ( h x , d a i + h b i + 1 ) h_x=max(h_x,d_{a_i}+h_{b_i}+1) hx=max(hx,dai+hbi+1)。
时间复杂度为并查集的复杂度。
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6;
int n, t;
int fa[N + 5], d[N + 5], ans[N + 5];
int find(int x)
{
if (x != fa[x])
{
int t = fa[x];
fa[x] = find(fa[x]);
d[x] +=d[t];
}
return fa[x];
}
void merge(int a,int b)
{
fa[b]=a;
d[b]=1;
int x=find(a);
ans[x]=max(ans[x],ans[b]+d[a]+1);
}
void solve()
{
cin >> n;
for (int i = 1; i <= n; i++)
{
fa[i] = i;
d[i] = 0;
ans[i] = 0;
}
vector<int> q;
for (int i = 1; i < n; i++)
{
int a, b, c;
cin >> a >> b >> c;
merge(a,b);
q.push_back(ans[c]);
}
for (int i = 0; i < n - 1; i++)
{
printf("%d ", q[i]);
}
printf("\n");
}
int main()
{
cin >> t;
while (t--)
{
solve();
}
return 0;
}
C. Sort4
Problem
给定长度为 n n n 的排列 a i a_i ai,在一次操作中,你可以选择至多四个元素并交换它们的位置,问最少需要多少次操作才能使得数组变成升序。
数据范围: 1 ≤ n ≤ 1 0 6 1\le n\le 10^6 1≤n≤106
Solution
因为给的是排列,那么位于 i i i 的元素最后需要到 a i a_i ai 的位置,因此考虑以 i → a i i\rightarrow a_i i→ai 建图。
显然建好的图一定是环的形状,可能有多个。记 l e n len len 表示环的长度。
当 l e n = 2 len =2 len=2 ,一次操作可以将两个这样的环排序好。
当 l e n = 3 , 4 len=3,4 len=3,4 ,一次操作只能将一个这样的环排序好。
当 l e n > 4 len>4 len>4 ,一次操作显然不可能将其排序好,需要多次操作。注意到每次操作可以将三个元素排序好,相当于 l e n − 3 len-3 len−3 ,一直这样操作直到 l e n ≤ 4 len\le 4 len≤4 。
时间复杂度: O ( n ) O(n) O(n)
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6;
int n, t;
int a[N + 5], vis[N + 5];
void solve()
{
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
vis[i] = 0;
}
int ans = 0, num = 0;
for (int i = 1; i <= n; i++)
{
int cnt = 0, p = i;
while (!vis[p])
{
vis[p] = 1;
cnt++;
p = a[p];
}
if (cnt == 2)
{
num++;
}
if (cnt > 2)
{
ans += cnt / 3;
if (cnt % 3 == 2)
{
num++;
}
}
}
ans += (num + 1) / 2;
printf("%d\n", ans);
}
int main()
{
cin >> t;
while (t--)
{
solve();
}
return 0;
}
F. Good Tree
Problem
假设当前有一颗树,树的边权为1,定义 f ( u ) = ∑ v d i s ( u , v ) f(u)=\sum_v dis(u,v) f(u)=∑vdis(u,v) , v v v 遍历树中的所有节点, d i s ( u , v ) dis(u,v) dis(u,v) 为树上节点 u u u 到节点 v v v 的最短路径。
一棵好树定义为存在两个节点 u u u 和 v v v 使得 f ( u ) − f ( v ) = x f(u)-f(v)=x f(u)−f(v)=x 。问一棵好树需要的最少节点个数是多少?
数据范围: 1 ≤ x ≤ 1 0 18 1\le x\le 10^{18} 1≤x≤1018
Solution
假设已经给定了两个节点 u , v u,v u,v 以及总共的节点个数为 n n n ,考虑怎么构造才能使得 f u − f v f_u-f_v fu−fv 的值最大。
记 k k k 表示当前两节点的最短路径,此时已经用了 k + 1 k+1 k+1 个节点,还剩下 n − k − 1 n-k-1 n−k−1 个节点。
显然这 n − k − 1 n-k-1 n−k−1 个节点都放到以 v v v 为根的子树中是最优的,此时 f ( u ) − f ( v ) = k × ( n − k − 1 ) ≤ ( n − 1 ) 2 4 f(u)-f(v)=k\times (n-k-1) \le \frac{(n-1)^2}{4} f(u)−f(v)=k×(n−k−1)≤4(n−1)2 ,由基本不等式 a b ≤ ( a + b ) 2 4 ab\le \frac{(a+b)^2}{4} ab≤4(a+b)2 可以得到最大值。
再通过对奇偶性进行讨论,可以得到如下结果:
当 n = 2 m n=2m n=2m 时, m a x = m ( m − 1 ) max=m(m-1) max=m(m−1)
当 n = 2 m + 1 n=2m+1 n=2m+1 时, m a x = m 2 max=m^2 max=m2
当 n = 2 ( m + 1 ) n=2(m+1) n=2(m+1) 时, m a x = ( m + 1 ) m max=(m+1)m max=(m+1)m
得到了节点个数固定时的最大之后,就可以对 x x x 进行讨论了。
当 m ( m − 1 ) < x ≤ m 2 m(m-1)<x \le m^2 m(m−1)<x≤m2 时, a n s = 2 m + 1 ans=2m+1 ans=2m+1,因为 n = 2 m n=2m n=2m 有两种情况可以到达最大值,一种是 k = m k=m k=m,另一种是 k = m − 1 k=m-1 k=m−1,然后还有一个节点可以在这条路径上移动,能够发现可以取到区间范围内的所有值。
当 m 2 < x ≤ ( m + 1 ) m m^2<x\le(m+1)m m2<x≤(m+1)m 时,因为 n = 2 m + 1 n=2m+1 n=2m+1 只有一种情况取到最大值,即 k = m + 1 k=m+1 k=m+1 ,此时再加一个节点能够取到的值和 m m m 的奇偶性有关。
如果 m m m 为奇数,只能取到如 m 2 + 1 , m 2 + 3 , … m^2+1,m^2+3,\dots m2+1,m2+3,… ,如果 m m m 为偶数,只能取到 m 2 + 2 , m 2 + 4 , … m^2+2,m^2+4,\dots m2+2,m2+4,… ,即能取到的值都为偶数。如果 x x x 为奇数,显然需要再加一个节点才可以。
时间复杂度: O ( l o g ( n ) ) O(log(n)) O(log(n))
Code
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e6;
int T;
ll x;
bool check(ll m)
{
ll a = m / 2;
ll ans;
if (m % 2 == 0)
{
ans = a * (a - 1);
}
else
{
ans = a * a;
}
return ans >= x;
}
void solve()
{
cin >> x;
ll l = 1, r = 2e9;
while (l <= r)
{
ll m = (l + r) >> 1;
if (check(m))
{
r = m - 1;
}
else
{
l = m + 1;
}
}
if (l % 2 == 0 && x % 2 == 1)
{
l++;
}
printf("%lld\n", l);
}
int main()
{
cin >> T;
while (T--)
{
solve();
}
return 0;
}
G. Horse Drinks Water
Problem
马在点 ( x G , y G ) (x_G,y_G) (xG,yG) ,帐篷在点 ( x T , y T ) (x_T,y_T) (xT,yT) ,同时 x x x 的正半轴和 y y y 的正半轴为河流,问马到河边喝完水再到帐篷的最短路径。
数据范围: 0 ≤ x G , y G , x T , y T ≤ 1 0 9 0\le x_G,y_G,x_T,y_T\le 10^9 0≤xG,yG,xT,yT≤109
Solution
分两种情况考虑马去 x x x 轴喝水和去 y y y 轴喝水的最短路径,然后取个 m i n min min 即可。
时间复杂度: O ( 1 ) O(1) O(1)
Code
#include<bits/stdc++.h>
#define ll long long
using namespace std;
int T=1;
ll xg,yg,xt,yt;
void solve()
{
cin>>xg>>yg>>xt>>yt;
ll xd=abs(xg-xt);
ll yd=abs(yg-yt);
double ans=min(sqrt(xd*xd+(yt+yg)*(yt+yg)),sqrt(yd*yd+(xt+xg)*(xt+xg)));
printf("%.15lf\n",ans);
}
int main()
{
cin>>T;
while(T--)
{
solve();
}
return 0;
}
H. Yet Another Origami Problem
Problem
给定长度为 n n n 的数组 a i a_i ai,每次可以选择执行其中一种操作:
-
选择一个数 p ( 1 ≤ p ≤ n ) p\ (1\le p\le n) p (1≤p≤n),对 a i ≤ a p a_i\le a_p ai≤ap的数,执行 a i = a i + 2 ( a p − a i ) a_i=a_i+2(a_p-a_i) ai=ai+2(ap−ai)
-
选择一个数 p ( 1 ≤ p ≤ n ) p\ (1\le p\le n) p (1≤p≤n),对 a i ≥ a p a_i\ge a_p ai≥ap的数,执行 a i = a i − 2 ( a i − a p ) a_i=a_i-2(a_i-a_p) ai=ai−2(ai−ap)。
问数组 a i a_i ai 的最大值与最小值之差最小为多少?
数据范围: 1 ≤ n ≤ 1 0 5 , 0 ≤ a i ≤ 1 0 16 1\le n\le10^5,0\le a_i\le10^{16} 1≤n≤105,0≤ai≤1016。
Solution
上述的操作其实就是将某个前缀或后缀进行翻转。因此先对数组 a i a_i ai进行升序排序。
当 n = 1 n=1 n=1 时,答案为 0 0 0。
当 n = 2 n=2 n=2 时,答案为 a 2 − a 1 a_2-a_1 a2−a1。
当 n = 3 n=3 n=3 时,每次 p p p 选择中间的数,并执行第一种操作,能够发现这种操作其实就是辗转相减法的实现,答案为 g c d ( a 2 − a 1 , a 3 − a 2 ) gcd(a_2-a_1,a_3-a_2) gcd(a2−a1,a3−a2)。
当 n > 3 n>3 n>3 时,每次 p p p 选择第二小的数,并执行第一种操作,这样一直操作下去得到的结果就是 g c d ( a 2 − a 1 , a 3 − a 2 , … , a n − a n − 1 ) gcd(a_2-a_1,a_3-a_2,\dots,a_n-a_{n-1}) gcd(a2−a1,a3−a2,…,an−an−1)。
时间复杂度: O ( n l o g ( n ) ) O(n\ log(n)) O(n log(n))
Code
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e5;
const ll M = 998244353;
int T = 1;
int n;
ll a[N + 5];
void solve()
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
scanf("%lld", &a[i]);
}
sort(a, a + n);
ll ans = 0;
for (int i = 1; i < n; i++)
{
ans = __gcd(ans, a[i] - a[i - 1]);
}
printf("%lld\n", ans);
}
int main()
{
cin >> T;
while (T--)
{
solve();
}
return 0;
}
I. Friends
Problem
现在有 n n n 个人从左到右站成一排,其中有 m m m 对朋友 ( u , v ) (u,v) (u,v)。问有多少个区间 [ l , r ] [l,r] [l,r] 满足区间内任意两人都是朋友。
数据范围: 1 ≤ n , m ≤ 1 0 6 , 1 ≤ u < v ≤ n 1\le n,m\le10^6,1\le u<v\le n 1≤n,m≤106,1≤u<v≤n
Solution
如果区间 [ l , r ] [l,r] [l,r] 是满足条件的,那么所有左端点为 l l l ,右端点小于等于 r r r 的区间也是满足条件的。因此对于左端点为 i i i 的区间,我们需要找到最大的右端点。
记 p i p_i pi 表示 i i i 与 i + 1 , i + 2 , … , p i i+1,i+2,\dots,p_i i+1,i+2,…,pi 为朋友,和 p i + 1 p_i+1 pi+1 不为朋友的位置。
那么左端点为 i i i 的区间,右端点最大为 m i n ( p i , p i + 1 , … , p n ) min(p_i,p_{i+1},\dots,p_n) min(pi,pi+1,…,pn)。
因此从后往前遍历,每次计算左端点为 i i i 的区间个数,同时更新后缀最小值即可。
时间复杂度: O ( m l o g ( m ) ) O(m\ log(m)) O(m log(m))
Code
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e5;
const ll M = 998244353;
int n, m;
int main()
{
cin >> n >> m;
ll ans = 0;
set<pair<int, int>> st;
for (int i = 1; i <= m; i++)
{
int u, v;
cin >> u >> v;
st.insert({u, v});
}
ll r = n;
for (int i = n; i >= 1; i--)
{
ll p = i + 1;
while (st.count({i, p}))
{
p++;
}
r = min(r, p - 1);
ans += r - i + 1;
}
printf("%lld\n", ans);
return 0;
}
J. Zero
Problem
给定一个长度为 n n n 的字符串 s s s,仅包含 0 , 1 , ? 0,1,? 0,1,? 三种字符。其中 ? ? ? 可以被等概率的替换为 0 0 0 或 1 1 1 。
对于区间 [ l , r ] [l,r] [l,r] 的子串,如果全为 1 1 1 那么值为 ( r − l + 1 ) k (r-l+1)^k (r−l+1)k,否则值为 0 0 0 。
字符串 s s s 的值为所有区间的值之和,问字符串 s s s 的值的期望是多少?
数据范围: 1 ≤ n ≤ 1 0 5 , 1 ≤ k ≤ 30 1\le n \le 10^5,1\le k \le 30 1≤n≤105,1≤k≤30
Solution
记 p i , j p_{i,j} pi,j 表示以 i i i 为右端点,连续 1 1 1 有 j j j 个的概率, f i f_i fi 表示所有以 i i i 为右端点的区间值之和。那么 f i = ∑ j ≥ 1 p i , j × j k f_i=\sum_{j\ge1}p_{i,j}\times j^k fi=∑j≥1pi,j×jk 。
当 s i = 0 s_i=0 si=0 时, f i = 0 f_i=0 fi=0 。
当 s i = 1 s_i=1 si=1 时,
f i = ∑ j ≥ 1 p i , j × j k = 1 + ∑ j − 1 ≥ 1 p i − 1 , j − 1 × j k = 1 + ∑ j − 1 ≥ 1 p i − 1 , j − 1 × ( j − 1 + 1 ) k = 1 + ∑ j − 1 ≥ 1 p i − 1 , j − 1 × ∑ r = 0 k ( k r ) ( j − 1 ) r = 1 + ∑ r = 0 k ( k r ) × ∑ j − 1 ≥ 1 p i − 1 , j − 1 × ( j − 1 ) r \begin{aligned} f_i & =\sum_{j\ge1}p_{i,j}\times j^k\\ & =1+\sum_{j-1\ge1}p_{i-1,j-1}\times j^k\\ &=1+\sum_{j-1\ge1}p_{i-1,j-1}\times (j-1+1)^k\\ & =1+\sum_{j-1\ge1}p_{i-1,j-1}\times \sum_{r=0}^k\binom {k}{r}(j-1)^r\\ & =1+\sum_{r=0}^k \binom{k}{r}\times \sum_{j-1\ge1}p_{i-1,j-1}\times (j-1)^r\\ \end{aligned} fi=j≥1∑pi,j×jk=1+j−1≥1∑pi−1,j−1×jk=1+j−1≥1∑pi−1,j−1×(j−1+1)k=1+j−1≥1∑pi−1,j−1×r=0∑k(rk)(j−1)r=1+r=0∑k(rk)×j−1≥1∑pi−1,j−1×(j−1)r
第二个等号是因为此时 s i = 1 s_i=1 si=1,所以有 p i , j = p i − 1 , j − 1 p_{i,j}=p_{i-1,j-1} pi,j=pi−1,j−1。
第四个等号是通过二项式定理得到的。
第五个等号就是交换求和顺序。
当 s i = ? s_i=? si=? 时,有 p i , j = p i − 1 , j − 1 2 p_{i,j}=\frac{p_{i-1,j-1}}{2} pi,j=2pi−1,j−1,因此 f i f_i fi 的值就是假设 s i = 1 s_i=1 si=1 情形的结果再除以 2 2 2 即可。
注意到 ∑ j − 1 ≥ 1 p i − 1 , j − 1 × ( j − 1 ) r \sum_{j-1\ge1}p_{i-1,j-1}\times (j-1)^r ∑j−1≥1pi−1,j−1×(j−1)r 与 f i − 1 = ∑ j ≥ 1 p i − 1 , j × j k f_{i-1}=\sum_{j\ge1}p_{i-1,j}\times j^k fi−1=∑j≥1pi−1,j×jk 的区别只在于幂次不同,因此考虑对 f i f_i fi 再加一维变成 f i , m = ∑ j ≥ 1 p i , j × j m f_{i,m}=\sum_{j\ge1}p_{i,j}\times j^m fi,m=∑j≥1pi,j×jm 。
那么 f i , m = 1 + ∑ r = 0 m ( m r ) f i − 1 , r f_{i,m}=1+\sum_{r=0}^m\binom{m}{r}f_{i-1,r} fi,m=1+∑r=0m(rm)fi−1,r,最后的答案即为 ∑ i = 1 n f i , k \sum_{i=1}^nf_{i,k} ∑i=1nfi,k 。
时间复杂度: O ( n k 2 ) O(nk^2) O(nk2)
Code
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e6;
const ll M = 998244353;
int n, k;
string s;
ll C[50][50], f[N][50];
ll fpow(ll a, ll b)
{
ll res = 1;
a %= M;
while (b)
{
if (b & 1)
{
res = res * a % M;
}
a = a * a % M;
b >>= 1;
}
return res;
}
int main()
{
cin >> n >> k;
cin >> s;
s = '0' + s;
for (int i = 0; i <= k; i++)
{
C[i][0] = C[i][i] = 1;
for (int j = 1; j < i; j++)
{
C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % M;
}
}
ll inv = fpow(2, M - 2), ans = 0;
for (int i = 1; i <= n; i++)
{
if (s[i] == '0')
{
continue;
}
for (int j = 0; j <= k; j++)
{
f[i][j] = 1;
for (int r = 0; r <= j; r++)
{
f[i][j] = (f[i][j] + C[j][r] * f[i - 1][r] % M) % M;
}
if (s[i] == '?')
{
f[i][j] = f[i][j] * inv % M;
}
}
ans = (ans + f[i][k]) % M;
}
printf("%lld\n", ans);
return 0;
}