2024牛客暑期多校训练营4
写在前面
比赛地址:https://ac.nowcoder.com/acm/contest/81599
以下按个人向难度排序。
妈的这场签到相当顺前五个题都没怎么卡,被 F 搞崩了妈的,幸好后面还是过了,J 到最后也没想到怎么把 \(k\) 扔到复杂度里太几把了呃呃
G
签到。
code by dztlb:
#include<bits/stdc++.h>
using namespace std;
#define int long long
int t;
double xg,yg,xt,yt;
double dis(double a,double b,double c,double d){
return sqrt((a-c)*(a-c)+(b-d)*(b-d));
}
signed main(){
cin>>t;
while(t--){
cin>>xg>>yg>>xt>>yt;
double ans=min(dis(xg,-1.00*yg,xt,yt),dis(-1.00*xg,yg,xt,yt));
printf("%.10lf\n",ans);
}
return 0;
}
/*
1
5 4 15
1 1 B
ABAB
BABA
ABAB
BABA
ABAB
*/
C
签到。
考虑最优的操作,显然放到正确位置上的数一定不会再被操作,则想到应当每次选择权值 \(i, a_{i}, a_{a_i}, a_{a_{a_i}}\),并将 \(i, a_i, a_{a_i}\) 三个权值放到正确的位置上。这样操作可使每次操作均将最多的数放到正确位置上,若不这样操作,则之后还要通过操作将它们放到正确位置上,则这样操作一定不会更劣。
太套路了,套路地转换成图论模型,节点 \(i\) 向节点 \(a_{i}\) 连边,由于排列的性质显然构成了若干环。则上述操作等价于在环上选择三个相连的点删掉,若删没了或删的只剩 1 个则说明经过上述操作即可将所有数归位,若剩下 2 个则还需要操作,但发现一次操作可以同时处理两个大小为 2 的环,于是需要特判一下。
记大小为 \(i\) 的环有 \(\operatorname{cnt}\) 个,答案即为:
code by dztlb:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+5;
int t;
int n;
int a[N],siz,st;
bool vis[N];
int c[5],top;
void dfs(int x){
vis[x]=1;
++siz;
if(x==st) return;
dfs(a[x]);
}
signed main(){
cin>>t;
while(t--){
cin>>n;
for(int i=1;i<=n;++i){
cin>>a[i];
vis[i]=0;
}
memset(c,0,sizeof(c));
int ans=0;
for(int i=1;i<=n;++i){
if(a[i]==i||vis[i]) continue;
siz=0;
st=i;
dfs(a[i]);
ans+=siz/3;
c[siz%3]++;
// cout<<siz<<endl;
}
ans+=c[2]/2;
if(c[2]%2==1) ans++;
cout<<ans<<'\n';
}
return 0;
}
/*
1
10
1 2 9 10 3 6 8 4 5 7
*/
I
枚举。
发现完全图的所有子图都是完全图,一个显然的想法是枚举题目要求的完全图的左右端点 \([l, r]\),则对于以 \(l\) 为左端点的极大的构成完全图的区间 \([l, r]\),当 \(l\) 递增时 \(r\) 一定不减。
于是考虑双指针枚举区间,发现仅需检查每次右端点 \(r+1\) 时,端点 \(r+1\) 与当前区间 \([l, r]\) 的点是否都有连边即可。可以对于每个端点 \(i\) 都维护有哪些编号小于 \(i\) 的点与它有连边,则对 \(r+1\) 二分检查不小于 \(l\) 的数是否是 \(r-l\) 个即可。
总时间复杂 \(O(n)\) 级别。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e6 + 10;
//=============================================================
int n, m, du[kN];
std::vector<int> edge[2][kN];
bool yes;
LL ans;
//=============================================================
bool check(int L_, int pos_) {
if (edge[0][pos_].empty()) return false;
int p = std::lower_bound(edge[0][pos_].begin(), edge[0][pos_].end(), L_) - edge[0][pos_].begin();
if (edge[0][pos_][p] != L_) return false;
int sz = edge[0][pos_].size() - p;
return (sz == (pos_ - L_));
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n >> m;
for (int i = 1; i <= n; ++ i) du[i] = 1;
for (int i = 1; i <= m; ++ i) {
int u, v; std::cin >> u >> v;
edge[0][v].push_back(u);
edge[1][u].push_back(v);
}
for (int i = 1; i <= n; ++ i) {
std::sort(edge[0][i].begin(), edge[0][i].end());
std::sort(edge[1][i].begin(), edge[1][i].end());
}
yes = 1;
int l = 1, r = 1;
for (; l <= n; ++ l) {
r = std::max(r, l);
while (r + 1 <= n && check(l, r + 1)) ++ r;
ans += 1ll * r - l + 1;
// del(l);
}
std::cout << ans << "\n";
return 0;
}
A
树,并查集。
钦定了每次询问的点都是树的根节点,一个很显然的想法是直接对于每棵树维护根的答案 \(\operatorname{ans}_{\operatorname{root}}\),考虑新连一条边的影响。
发现每次连边均为将一个根 \(u_1\) 接到另外一棵根为 \(u_2\) 的树的的节点 \(v\) 上,则根 \(u_2\) 的答案要么不变,要么更新为 \(\operatorname{dis}(u_2, v) + \operatorname{ans}_{u_1}\)。
\(\operatorname{dis}\) 即原树上两点深度差,维护根使用并查集即可。总时间复杂度 \(O(n)\) 级别。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e6 + 10;
//=============================================================
int n, ans[kN], fa[kN], directfa[kN], dep[kN];
int a[kN], b[kN], c[kN];
//=============================================================
int find(int x_) {
return (fa[x_] == x_) ? (x_) : (fa[x_] = find(fa[x_]));
}
void merge(int x_, int y_) {
int fx = find(x_), fy = find(y_);
fa[fx] = fy;
}
void dfs(int u_) {
if (!directfa[u_]) {
dep[u_] = 1;
return ;
}
if (dep[u_]) return ;
dfs(directfa[u_]);
dep[u_] = dep[directfa[u_]] + 1;
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
std::cin >> n;
for (int i = 1; i <= n; ++ i) dep[i] = directfa[i] = 0;
for (int i = 1; i < n; ++ i) {
std::cin >> a[i] >> b[i] >> c[i];
directfa[b[i]] = a[i];
}
for (int i = 1; i <= n; ++ i) if (!dep[i]) dfs(i);
for (int i = 1; i <= n; ++ i) ans[i] = 1, directfa[i] = 0, fa[i] = i;
for (int i = 1; i < n; ++ i) {
merge(b[i], a[i]);
directfa[b[i]] = a[i];
int top = find(a[i]), u = a[i], delta = ans[b[i]];
ans[top] = std::max(ans[top], dep[u] - dep[top] + 1 + delta);
std::cout << ans[c[i]] - 1 << " ";
}
std::cout << "\n";
}
return 0;
}
H
思维,数学,辗转相减法。
首先重复权值是没用的,先去个重,排个序,特判下 \(n=1\) 时答案为 0。
发现一种非常好的操作方案是每次选择次大值或次小值作为 \(a_{p}\),然后仅对最大值/最小值进行操作从而减少整个数列的极差。
发现当 \(n=3\) 时,这个过程相当于不断地对两种差值做辗转相减;扩展到 \(n\) 更大,发现这相当于被操作的最大值/最小值之外的所有部分,与最大值/最小值的差值做辗转相减,则答案实际上即辗转相减的结果——排序后整个数列相邻之差的 \(\operatorname{gcd}\)。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+5;
int t;
int n,ans;
int a[N];
signed main(){
cin>>t;
while(t--){
cin>>n;
for(int i=1;i<=n;++i){
cin>>a[i];
}
sort(a+1,a+1+n);
if(n==1){
puts("0"); continue;
}
ans=a[2]-a[1];
for(int i=2;i<n;++i){
int t=a[i+1]-a[i];
ans=__gcd(ans,t);
}
cout<<ans<<'\n';
}
return 0;
}
/*
2
3
1 3 100
4
1 1 1 1
*/
F
构造。
场上构造的太简单了呃呃就只证出来个上界,后面试了几发猜了个暴论特判过了呃呃呃
发现使用 \(n\) 个点构造出的 \(x\) 的上界在链时取到,但界内可能有些 \(x\) 无法构造得到,但是发现只要再加 1 个点一定可以。手玩了 \(n=7,8,9\) 的例子发现需要保证取到上界的 \(n\) 若与 \(x\) 奇偶性不同则无法构造得到需要加 1,若奇偶性相同则可以于是特判过了呃呃
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e7+5;
int t,n,x;
int getans(int n){
if(n%2==0){
return n*(n-1)/2-(1+(n-2)/2)*(n-2)/2-n/2;
}else return n*(n-1)/2-(1+(n-1)/2)*(n-1)/2;
}
signed main(){
cin>>t;
while(t--){
cin>>x;
if(x<=3){
cout<<x+2<<'\n'; continue;
}
int l=5,r=2100000000ll,ans=0;
while(l<=r){
int mid=(l+r)>>1;
if(getans(mid)>=x){
ans=mid;
r=mid-1;
}else l=mid+1;
}
if(getans(ans)%2==x%2)cout<<ans<<'\n';
else if(ans%2==0&&x%2==1) cout<<ans+1<<'\n';
else cout<<ans<<'\n';
}
return 0;
}
/*
2
3
1 3 100
4
1 1 1 1
*/
J
枚举,数学
场上想出了一万个 \(O(n^2)\) 做法呃呃呃呃太傻比了、、、不过要是敢交一发就过了也是够搞、、、
以下我的推法和题解本质相同但是状态和枚举顺序不太一样,注意不要搞混了、、、
一个显然的想法是考虑每种长度的全 1 区间的期望数量有多少。发现在计算期望数量时需要考虑区间内有多少 ?
以考虑其出现概率,于是考虑在枚举区间右端点的同时维护下区间内有多少 ?
,可以想到一个显然的二维 DP 状态 \(f_{i, \operatorname{len}}\) 表示区间右端点为 \(i\),区间长度为 \(\operatorname{len}\) 的区间的期望数量,初始化 \(f_{i, 0} = 1\),则有显然的转移:
考虑记 \(g_{i}\) 表示以 \(i\) 为右端点的区间在给定的 \(k\) 情况下对答案的贡献之和,考虑枚举右端点为 \(i\) 的全 1 区间的长度 \(\operatorname{len}\),则有:
则最终答案即为:
发现状态数和转移数都是 \(O(n^2)\) 的,看着就非常不可直接做的样子呃呃,但考虑将 \(f\) 的转移代入尝试将两个状态合并,以 \(s_i=1\) 时 \(f_{i, \operatorname{len}} = f_{i - 1, \operatorname{len} - 1}\) 为例,有:
感觉这里面可以抠出一个 \(g_{i - 1}\),再考虑二项式定理展开尝试一下:
后面这一坨什么玩意儿?再拿出来推一下:
发现上面第二个求和符号里的式子好熟悉——这不是当给定常数 \(k=l\) 时 \(g_i\) 的定义式吗!于是考虑修改 \(g\) 的状态,设 \(g_{i, j}\) 表示以 \(i\) 为右端点的区间在给定常数 \(k=j\) 情况下对答案的贡献之和,整理一下,则当 \(s_{i} = 1\) 时有:
\(s_{i} = \text{?}\) 时同理,仅需乘个系数 \(\frac{1}{2}\) 即可:
则最终答案即为:
上述状态数量为 \(O(nk)\) 级别,转移数为 \(O(nk^2)\) 级别,已经可以直接做了。预处理下组合数,初始化所有 \(g_{i,j}=0\),转移时枚举位置 \(i\) 与指数 \(j\) 大力转移即可,总时间复杂度为 \(O(nk^2)\) 级别。
我们将 \(O(n^2)\) 暴力优化到 \(O(nk^2)\) 的牛逼,其本质是什么呢?观察我们上面 \(g_{i}\) 的转移形式的前后两种形式的转化:我们将直接大力枚举区间长度,转化成了用上一个位置的状态 \(g_{i-1}\) 考虑后面添加一个 1 的影响,即对于所有长为 \(\operatorname{len}\) 的全 1 串的贡献 \(\operatorname{len}^k\) 转化为 \((\operatorname{len}+1)^k\),考虑把它们的差 \((\operatorname{len}+1)^k - \operatorname{len}^k\) 使用二项式定理拆开,从而仅需维护额外与指数大小有关的若干状态 \(g_{i-1,0}\sim g_{i-1,k}\) 即可转移。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e5 + 10;
const int kK = 31;
const LL p = 998244353;
//=============================================================
int n, k;
std::string s;
LL C[kK][kK], g[kN][kK];
//=============================================================
void init() {
C[0][0] = 1;
for (int i = 1; i <= k; ++ i) {
C[i][0] = C[i][i] = 1ll;
for (int j = 1; j < i; ++ j) {
C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % p;
}
}
}
LL qpow(LL x_, LL y_) {
LL ret = 1;
while (y_) {
if (y_ & 1) ret = ret * x_ % p;
x_ = x_ * x_ % p, y_ >>= 1ll;
}
return ret;
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n >> k;
std::cin >> s; s = "$" + s;
init();
LL ans = 0, inv2 = qpow(2, p - 2);
for (int i = 1; i <= n; ++ i) {
if (s[i] == '0') {
for (int j = 0; j <= k; ++ j) g[i][j] = 0;
continue;
}
LL aplha = (s[i] == '?') ? inv2 : 1ll;
for (int j = 0; j <= k; ++ j) {
g[i][j] = 1;;
for (int l = 0; l <= j; ++ l) g[i][j] += C[j][l] % p * g[i - 1][l] % p, g[i][j] %= p;
g[i][j] = aplha * g[i][j] % p;
}
ans = (ans + g[i][k]) % p;
}
std::cout << ans << "\n";
return 0;
}
/*
3 2
111
2 2
11
3 1
00?
*/
写在最后
参考:2024牛客暑期多校训练营4 - 空気力学の詩 - 博客园。
学到了什么:
- A:注意特殊条件!!!
- H:观察操作的性质,并做等价转化。
- J:考虑复杂度里是不是应该出现点什么东西;\(i^k\) 贡献转 \((i+1)^k\) 贡献考虑套路地二项式定理拆差值。
哈哈又到了我最喜欢的夹带私货环节,我草国服晄轮大祭 PV 还重置了我去这个质量太几把高了比日服原 PV 厉害到不知道哪里去了。客观地说,某四字游戏国服在本地化在二游本地化方面做出了一些值得肯定的创新性的贡献,其他就懒得评价了。