Petrozavodsk Winter 2023. Day 7: Gennady Korotkevich Contest 7
Preface
连着两天被这种高难度场折磨了,昨天5题今天4题没绷住……
这场开局还好,磕磕绊绊地在 150min 的时候出了三个题,然后后面就让机给徐神和远程连线的祁神写 C 和 F 了
后面对着 D 题想了好久还是不会做,索性去帮徐神的 C 题当 Debugger 抓出了一堆唐氏错误
最后祁神写分讨几何 F 一直 WA,我也进入了晚饭觅食状态没干啥事就结束了,然后经典比赛结束后立刻过题
等后天祁神线下回归之后再看看吧,要记 Rating 的多校也要开始了,多少得加把劲的说
A. Classical A+B Problem
签到,设 \(n\) 由数字 \(d\) 开头,位数为 \(m\)
则其中一个数要么是 \(m\) 个 \(d\);要么是 \(m\) 个 \(d-1\);注意后面一种情况当 \(d=1\) 时特化为 \(m-1\) 个 \(9\)
然后写个高精验证下结果即可
#include<cstdio>
#include<iostream>
#include<string>
#include<algorithm>
#include<assert.h>
#define RI register int
#define CI const int&
using namespace std;
const int N=4005;
int t,n; string s;
int main()
{
ios::sync_with_stdio(false); cin.tie(0);
for (cin>>t;t;--t)
{
auto sub=[&](string A,string B)
{
reverse(A.begin(),A.end());
reverse(B.begin(),B.end());
static int tmp[N]; int c=0;
for (RI i=0;i<A.size();++i)
{
int res=A[i]-(i<B.size()?B[i]:'0')-c;
if (res<0) res+=10,c=1; else c=0;
tmp[i]=res;
}
int len=A.size()-1;
while (tmp[len]==0) --len;
string C; for (RI i=len;i>=0;--i) C+=tmp[i]+'0';
return C;
};
RI i; cin>>s; string A(s.size(),s[0]);
if (A<s)
{
string B=sub(s,A); bool flag=1;
for (i=1;i<B.size();++i)
if (B[i]!=B[0]) { flag=0; break; }
if (flag) { cout<<A<<' '<<B<<endl; continue; }
}
if (s[0]>='2') A=string(s.size(),s[0]-1);
else A=string(s.size()-1,'9');
string B=sub(s,A); bool flag=1;
for (i=1;i<B.size();++i)
if (B[i]!=B[0]) { flag=0; break; }
if (flag) { cout<<A<<' '<<B<<endl; continue; }
assert(0);
}
return 0;
}
C. Classical Data Structure Problem
DS 逃兵闪总出列!写不来 FHQ_Treap 跑路丢给徐神写,转头去开 Counting;结果后面竟然两边都神奇地开出来了可海星
直觉的做法是动态开点线段树,但算一下显然会爆空间;但可以维护区间的不止线段树这一类二叉树,还有诸如 FHQ-Treap 或者 Splay 这样的 BST
因此考虑用 FHQ-Treap 维护区间,和传统的 FHQ-Treap 不同的是每个节点存储的不是一个元素的值,而是一段所有元素均相同的区间
注意到每次操作最多新增两个区间(即两个节点),因此空间复杂度为 \(O(n)\),可以通过此题
#include <bits/stdc++.h>
constexpr int $n = 500000;
namespace Treap {
void debug(int);
std::mt19937 rng(/*(std::random_device())()*/114515);
struct TreapNode {
int lc, rc;
int l, r;
int tree_l, tree_r;
uint32_t val, sum, tag;
uint32_t w, siz;
} node[$n * 2 + 5];
inline TreapNode& $(size_t i) { return node[i]; }
int O = 1;
int newnode(int l, int r) {
int res = O++;
$(res).lc = $(res).rc = 0;
$(res).l = $(res).tree_l = l;
$(res).r = $(res).tree_r = r;
$(res).val = $(res).sum = $(res).tag = 0;
$(res).w = rng();
$(res).siz = 1;
return res;
}
void pushup(int i) {
$(i).sum = $(i).val * uint32_t($(i).r - $(i).l + 1) + $($(i).lc).sum + $($(i).rc).sum;
$(i).siz = 1 + $($(i).lc).siz + $($(i).rc).siz;
if($(i).lc) $(i).tree_l = $($(i).lc).tree_l; else $(i).tree_l = $(i).l;
if($(i).rc) $(i).tree_r = $($(i).rc).tree_r; else $(i).tree_r = $(i).r;
return ;
}
void pushdown(int i) {
if($(i).tag) {
if($(i).lc) {
TreapNode &lc = $($(i).lc);
lc.val += $(i).tag;
lc.tag += $(i).tag;
lc.sum += $(i).tag * (lc.tree_r - lc.tree_l + 1);
}
if($(i).rc) {
TreapNode &rc = $($(i).rc);
rc.val += $(i).tag;
rc.tag += $(i).tag;
rc.sum += $(i).tag * (rc.tree_r - rc.tree_l + 1);
}
$(i).tag = 0;
}
}
int merge(int l, int r) {
if(!l) return r; if(!r) return l;
pushdown(l), pushdown(r);
if($(l).w > $(r).w) {
$(l).rc = merge($(l).rc, r);
pushup(l); return l;
} else {
$(r).lc = merge(l, $(r).lc);
pushup(r); return r;
}
}
int merge(int l, int m, int r) {
return merge(merge(l, m), r);
}
std::pair<int, int> split_range(int nd, int d) {
if(!nd) return {0, 0};
if(d >= $(nd).r) return {nd, 0};
if(d < $(nd).l) return {0, nd};
assert($(nd).l <= d && $(nd).r > d);
assert($(nd).siz == 1);
assert($(nd).lc == 0 && $(nd).rc == 0);
assert($(nd).tree_l == $(nd).l && $(nd).tree_r == $(nd).r);
int nn = newnode(d + 1, $(nd).r);
$(nn).val = $(nd).val;
pushup(nn);
$(nd).r = d;
pushup(nd);
return {nd, nn};
}
std::pair<int, int> split_by(int nd, std::function<bool(int)> is_left) {
// std::cerr << "nd = " << nd << char(10);
if(!nd) return {0, 0};
pushdown(nd);
if(is_left(nd)) {
auto [m, r] = split_by($(nd).rc, is_left);
$(nd).rc = m; pushup(nd);
return {nd, r};
} else {
auto [l, m] = split_by($(nd).lc, is_left);
$(nd).lc = m; pushup(nd);
return {l, nd};
}
}
std::pair<int, int> split_by_rank(int nd, int rk) {
return split_by(nd, [&rk] (int nd) {
if($($(nd).lc).siz + 1 <= rk) {
rk -= $($(nd).lc).siz + 1;
return true;
} else return false;
});
}
std::tuple<int, int, int> fetch_range(int nd, int lb, int rb) {
// std::cerr << "lb, rb = " << lb << ", " << rb << char(10);
auto [L, MR] = split_by(nd, [lb] (int nd) {
return $(nd).r < lb;
});
auto [M, R] = split_by(MR, [rb] (int nd) {
return $(nd).l <= rb;
});
// debug(L);
// debug(M);
// debug(R);
auto [lm, t1] = split_by_rank(M, 1);
auto [l, t2] = split_range(lm, lb - 1);
// std::cerr << "[debug] l = " << l << char(10);
int M2 = merge(t2, t1);
auto [t3, mr] = split_by_rank(M2, $(M2).siz - 1);
auto [t4, r] = split_range(mr, rb);
int M3 = merge(t3, t4);
return {merge(L, l), M3, merge(r, R)};
}
void debug(int nd) {
std::cerr << "[debug] ";
if(nd == 0) std::cerr << "nd = null\n";
else {
std::cerr << "nd = " << nd << ":\n";
std::cerr << " l = " << $(nd).l << "\n";
std::cerr << " r = " << $(nd).r << "\n";
std::cerr << " tree_l = " << $(nd).tree_l << "\n";
std::cerr << " tree_r = " << $(nd).tree_r << "\n";
}
std::cerr << "----------\n";
}
}
int n, m, p, q;
uint32_t x = 0;
int main(void) {
// freopen("C2.in", "r", stdin);
using Treap::$;
std::cin >> n >> m;
int root = Treap::newnode(0, (1 << m) - 1);
for(uint32_t i = 1; i <= n; ++i) {
std::cin >> p >> q;
p = p + x & (1 << m) - 1;
q = q + x & (1 << m) - 1;
int l = p, r = q;
if(l > r) std::swap(l, r);
auto [L, M, R] = Treap::fetch_range(root, l, r);
// Treap::debug(L);
// Treap::debug(M);
// Treap::debug(R);
// assert($(M).tree_l == l);
// assert($(M).tree_r == r);
$(M).val += i;
$(M).tag += i;
$(M).sum += i * uint32_t($(M).tree_r - $(M).tree_l + 1);
x += $(M).sum;
// std::cerr << "x = " << x << char(10);
root = Treap::merge(L, M, R);
}
std::cout << (x & (1 << 30) - 1) << char(10);
return 0;
}
D. Classical DP Problem
这题大致思路都有了,但没想到题解中钦定两个条件必定成立其一来简化状态,还是太菜太菜
首先不难发现要求最少的车数量只需要从左下角一路往右上放,直到放不下了为止,不妨把最少需要的车数量记为 \(k\)
对于左下角的 \(k\times k\) 的矩形,注意到一种合法的放置方案必然满足以下条件之一:
- 每一行都恰好有一个车;
- 每一列都恰好有一个车;
考虑如果某个方案同时不满足上面两个条件,则必然存在某个格子不被车覆盖,因此不合题意
那我们计算答案只需要用满足条件一的方案数加上满足条件二的方案数,再减去同时满足两个条件的方案数即可,后者显然就是 \(k!\)
以求解符合条件一的方案数为例,由于我们知道了此时必然每行都有一个车,因此 DP 状态就很好设计了
令 \(m\) 表示在该矩形外有多少列需要覆盖,我们把这些列称为关键列;同时为了方便起见将 \(\{a\}\) 序列翻转
令 \(f_{i,j}\) 表示处理了前 \(i\) 行,且 覆盖了 \(j\) 个关键列的方案数,则有转移:
- 第 \(i+1\) 行选择放一个车在关键列,则 \(f_{i+1,j+1}\leftarrow f_{i,j}\times (m-j)\);
- 第 \(i+1\) 行选择放一个车在非关键列,则 \(f_{i+1,j}\leftarrow f_{i,j}\times (a_{i+1}-(m-j))\)
求解符合条件二的方案数只需要将这个杨表转置后即可用上面的方法求解,总复杂度 \(O(n^2)\)
#include<cstdio>
#include<iostream>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=5005,mod=998244353;
int n,k=1,a[N],b[N],f[N][N];
inline int solve(CI k,CI m)
{
RI i,j; for (i=0;i<=k;++i) for (j=0;j<=m;++j) f[i][j]=0;
for (f[0][0]=1,i=0;i<k;++i) for (j=0;j<=m;++j) if (f[i][j])
{
(f[i+1][j+1]+=1LL*f[i][j]*(m-j)%mod)%=mod;
(f[i+1][j]+=1LL*f[i][j]*(a[i+1]-(m-j))%mod)%=mod;
}
return f[k][m];
}
int main()
{
RI i; for (scanf("%d",&n),i=1;i<=n;++i) scanf("%d",&a[i]);
reverse(a+1,a+n+1);
for (i=1;i<=n;++i) if (a[i]>=i&&a[i+1]<i+1) k=i;
int ans=solve(k,a[k+1]);
for (i=1;i<=n;++i) ++b[a[i]];
for (i=n-1;i>=1;--i) b[i]+=b[i+1];
for (i=1;i<=n;++i) a[i]=b[i];
ans=(ans+solve(k,a[k+1]))%mod;
int fact=1; for (i=1;i<=k;++i) fact=1LL*fact*i%mod;
return printf("%d %d",k,(ans-fact+mod)%mod),0;
}
F. Classical Geometry Problem
思路没啥困难的分讨几何题,但需要注意一些 Corner Case
首先假设是从目标点走回原点,不难发现通过选择适当的顶点总可以走到上下两个底面中的一个
而走到一个面上后可以根据这个点在面的四个区域,将该点走到某条棱上;之后的操作就可以沿着棱一路走回原点了
#include<bits/stdc++.h>
using namespace std;
#define int long long
using LD = long double;
LD eps = 1e-8;
LD sqr(LD x){return x*x;}
int sgn(LD x){return fabs(x)<=eps ? 0 : (x>eps ? 1 : -1);}
struct Pt{
LD x, y, z;
Pt operator*(const LD &b)const{return Pt{x*b, y*b, z*b};}
Pt operator-(const Pt &b)const{return Pt{x-b.x, y-b.y, z-b.z};}
Pt operator+(const Pt &b)const{return Pt{x+b.x, y+b.y, z+b.z};}
LD len()const{return sqrt(x*x+y*y+z*z);}
Pt unit()const{return (*this)*(1/len());}
};
int t;
signed main(){
ios::sync_with_stdio(0); cin.tie(0);
cout << setiosflags(ios::fixed) << setprecision(10);
cin >> t;
while (t--){
Pt T0, C0, V0, T1, C1, V1, T2;
int tz1, ty2;
int m=3;
cin >> T0.x >> T0.y >> T0.z;
if (sgn(T0.x)==0 && sgn(T0.y)==0){
printf("1\n");
printf("0 0 255 %.10Lf\n", T0.z);
continue;
}
C0.x = (T0.x <= 127 ? 0 : 255);
C0.y = (T0.y <= 127 ? 0 : 255);
C0.z = (T0.z > 127 ? 0 : 255);
tz1 = (T0.z <= 127 ? 0 : 255);
if (255==tz1) ++m;
LD tt1;
if (sgn(T0.z-C0.z)==0) tt1=0;
else{
V0 = (T0-C0).unit();
tt1 = (tz1-T0.z)/V0.z;
}
T1 = T0 + V0*tt1;
C1.z = tz1;
C1.x = (sgn(T1.y-T1.x)>=0 ? 0 : 255);
C1.y = (sgn(T1.y-T1.x)>=0 ? 0 : 255);
ty2 = (sgn(T1.y-T1.x)<0 ? 0 : 255);
if (255==ty2) ++m;
LD tt2;
if (sgn(T1.y-C1.y)==0) tt2=0;
else{
V1 = (T1-C1).unit();
tt2 = (ty2-T1.y)/V1.y;
}
T2 = T1 + V1*tt2;
// printf("tz1=%lld ty2=%lld\n", tz1, ty2);
// printf("T2(%Lf %Lf %Lf)\n", T2.x, T2.y, T2.z);
// printf("V1(%Lf %Lf %Lf)\n", V1.x, V1.y, V1.z);
// printf("T1(%Lf %Lf %Lf)\n", T1.x, T1.y, T1.z);
// printf("V0(%Lf %Lf %Lf)\n", V0.x, V0.y, V0.z);
// printf("T0(%Lf %Lf %Lf)\n", T0.x, T0.y, T0.z);
printf("%lld\n", m);
if (tz1==255) printf("0 0 255 255\n");
if (ty2==255) printf("0 255 %lld 255\n", tz1);
printf("255 %lld %lld %.10Lf\n", ty2, tz1, abs(T2.x));
// cout << 255 << ' ' << ty2 << ' ' << tz1 << ' ' << abs(T2.x) << '\n';
printf("%lld %lld %lld %.10Lf\n", (int)C1.x, (int)C1.y, (int)C1.z, abs(tt2));
// cout << C1.x << ' ' << C1.y << ' ' << C1.z << ' ' << abs(tt2) << '\n';
printf("%lld %lld %lld %.10Lf\n", (int)C0.x, (int)C0.y, (int)C0.z, abs(tt1));
// cout << C0.x << ' ' << C0.y << ' ' << C0.z << ' ' << abs(tt1) << '\n';
}
return 0;
}
H. Classical Maximization Problem
很有意思的一个题,本来以为是个网络流,后面发现其实是个构造
首先不妨将每个行看作一个点,每个列也看作一个点,同时将原来平面上的点看作连接对应行列点的边
然后我们就得到了一个二分图,此时需要给上面所有的边两两匹配,要求匹配的两条边需要有公共点
这个问题看起来很像网络流,但实际上我们可以证明,对于一个有 \(m\) 条边的连通块,总存在一种方案使得匹配数为 \(\lfloor \frac{m}{2} \rfloor\)
首先不妨考虑连通块为树的情况,我们只需要从叶子向上一路匹配:
如果某个点有向下有偶数条边,则直接将它们两两配对;否则两两配对后再把剩下的一条边和该点的父边匹配
而对于连通块为图的情况,只需要建出该图的一个 DFS 生成树,然后把所有的返祖边挂在深度较大的点上,然后继续套用上面的匹配过程即可
#include<cstdio>
#include<iostream>
#include<vector>
#include<algorithm>
#include<utility>
#include<array>
#define RI register int
#define CI const int&
using namespace std;
typedef pair <int,int> pi;
const int N=400005;
int t,n,nx,ny,x[N],y[N],rst[N],vis[N],dep[N],used[N];
vector <pi> v[N],tr[N],ans; vector <int> low[N],rub;
vector <array <int,3>> untr;
inline void addedge(CI x,CI y,CI z)
{
v[x].push_back(pi(y,z)); v[y].push_back(pi(x,z));
}
inline void DFS1(CI now,CI fa=0)
{
vis[now]=1; dep[now]=dep[fa]+1;
for (auto [to,id]:v[now])
if (to!=fa)
{
if (!vis[to]) tr[now].push_back(pi(to,id)),DFS1(to,now);
else if (!used[id]) used[id]=1,untr.push_back({now,to,id});
}
}
inline bool DFS2(CI now,CI lst=0)
{
vector <int> son;
for (auto [to,id]:tr[now])
if (DFS2(to,id)) son.push_back(id);
for (auto x:low[now]) son.push_back(x);
reverse(son.begin(),son.end());
if (son.size()%2)
{
for (RI i=0;i+1<son.size();i+=2)
ans.push_back(pi(son[i],son[i+1]));
if (lst)
{
ans.push_back(pi(lst,son.back()));
return 0;
} else
{
rub.push_back(son.back());
return 1;
}
} else
{
for (RI i=0;i+1<son.size();i+=2)
ans.push_back(pi(son[i],son[i+1]));
return 1;
}
}
int main()
{
//freopen("H.in","r",stdin);
for (scanf("%d",&t);t;--t)
{
RI i; for (scanf("%d",&n),i=1;i<=2*n;++i) scanf("%d%d",&x[i],&y[i]);
for (i=1;i<=2*n;++i) rst[i]=x[i];
sort(rst+1,rst+2*n+1); nx=unique(rst+1,rst+2*n+1)-rst-1;
for (i=1;i<=2*n;++i) x[i]=lower_bound(rst+1,rst+nx+1,x[i])-rst;
for (i=1;i<=2*n;++i) rst[i]=y[i];
sort(rst+1,rst+2*n+1); ny=unique(rst+1,rst+2*n+1)-rst-1;
for (i=1;i<=2*n;++i) y[i]=lower_bound(rst+1,rst+ny+1,y[i])-rst;
for (i=1;i<=nx+ny;++i) v[i].clear(),tr[i].clear(),low[i].clear(),vis[i]=0;
for (i=1;i<=2*n;++i) used[i]=0,addedge(x[i],nx+y[i],i);
ans.clear(); rub.clear();
for (i=1;i<=nx+ny;++i) if (!vis[i])
{
untr.clear(); DFS1(i);
for (auto [x,y,id]:untr)
{
if (dep[x]<dep[y]) swap(x,y);
low[x].push_back(id);
}
DFS2(i);
}
printf("%d\n",ans.size());
for (auto [x,y]:ans) printf("%d %d\n",x,y);
for (i=0;i+1<rub.size();i+=2) printf("%d %d\n",rub[i],rub[i+1]);
}
return 0;
}
K. Classical Summation Problem
很优美的 Counting 题,需要一些灵光乍现和神奇转化
原题要我们求所有方案的中位数的编号和,不妨稍作转化改为求所有方案中位数编号的期望 \(E(M)\),最后乘上 \(n^k\) 就是答案
对于 \(k\) 为奇数的情况,不难发现由于中位数点恒在中心,因此具有对称性,最后的 \(E(M)=\frac{n+1}{2}\)
而 \(k\) 为偶数的情况则会导致中位数距离中心偏左,失去了对称性就不能套用上面的结论了
但我们不妨假设有一个虚拟中点位于 \(a_{\frac{k}{2}}\) 和 \(a_{\frac{k}{2}+1}\) 之间,这个中点位置的期望亦为 \(E(M')=\frac{n+1}{2}\)
现在如果我们能求出 \(a_{\frac{k}{2}}\) 和 \(a_{\frac{k}{2}+1}\) 之间距离的期望 \(E(d)\),则 \(E(M)=E(M')-\frac{E(d)}{2}\),问题就解决了
\(E(d)\) 乍一看不好处理,但由于这题的距离都是由实在的边构成的,因此可以拆贡献
考虑 \(i\leftrightarrow i+1\) 这条边成为 \(a_{\frac{k}{2}}\) 和 \(a_{\frac{k}{2}+1}\) 之间的边的概率,不难发现其贡献显然为 \(C_{k}^{\frac{k}{2}}\times (i-1)^{\frac{k}{2}}\times (n-i)^{\frac{k}{2}}\)
累加后即可得到 \(E(d)\) 的值,从而解决原问题
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=1e6+5,mod=998244353,inv2=(mod+1)/2;
int n,k;
inline int quick_pow(int x,int p=mod-2,int mul=1)
{
for (;p;p>>=1,x=1LL*x*x%mod) if (p&1) mul=1LL*mul*x%mod; return mul;
}
int main()
{
RI i; scanf("%d%d",&n,&k);
if (k%2==1) return printf("%d",1LL*quick_pow(n,k)*(n+1)%mod*inv2%mod),0;
int comb=1; for (i=1;i<=k/2;++i) comb=1LL*comb*(k-i+1)%mod*quick_pow(i)%mod;
int d=0; for (i=1;i<n;++i) (d+=1LL*quick_pow(i,k/2)*quick_pow(n-i,k/2)%mod)%=mod;
d=1LL*d*comb%mod*quick_pow(quick_pow(n,k))%mod;
d=(1LL*(n+1)*inv2%mod-1LL*d*inv2%mod+mod)%mod;
return printf("%d",1LL*quick_pow(n,k)*d%mod),0;
}
Postscript
明天还有一天双人作战,希望能来点阳间场的说