第九届中国大学生程序设计竞赛 深圳站(CCPC 2023 Shenzhen Site)
Preface
由于这周末大家都要补课,因此只能把训练挪到周五晚上了
然后祁神打了2h就跑路去上课了,前期我和徐神也因为成都站相关的一些准备工作被迫脱离了比赛
因此最后发现机时不够会写的 D 赛后 30min 过了,感觉正常打的话应该能出 9 题
A. A Good Problem
考虑按值域分治,从二进制位从高到低考虑,先把这一位上为 \(1\) 的数对应的下标位置用二操作加 \(1\),然后集体用一操作加到这一位上为 \(1\),递归处理即可
总操作次数是 \(O(n\log n)\) 级别的
#include<bits/stdc++.h>
using namespace std;
using pii = pair<int, int>;
const int N = 1005;
int n, A[N];
vector<int> vec[20];
vector<pii> ans;
void merge(vector<int> &vec, int bit) {
// printf("bit:%d:", bit); for (int x : vec) printf("%d ", x); puts("");
if (bit<0) return;
if (vec.empty()) return;
vector<int> a, b;
for (int x : vec) {
if ((A[x]>>bit)&1) a.push_back(x);
else b.push_back(x);
}
if (!a.empty()) {
for (int x : a) ans.push_back({2, x});
int val1 = 0;
int val2 = 0;
for (int i=9; i>bit; --i) if ((A[a[0]]>>i)&1) val2 += (1<<i);
val1 = val2+1;
val2 += (1<<bit);
for (int i=val1; i<val2; ++i) ans.push_back({1, i});
// printf("val=%d val2=%d\n", val1, val2);
merge(a, bit-1);
}
merge(b, bit-1);
}
signed main() {
ios::sync_with_stdio(0); cin.tie(0);
cin >> n;
vector<int> vec;
for (int i=1; i<=n; ++i) cin >> A[i], vec.push_back(i);
merge(vec, 9);
cout << ans.size() << '\n';
for (auto [a, b] : ans) cout << a << ' ' << b << '\n';
return 0;
}
D. Bot Brothers
很神秘的一个题,最后时刻被徐神一眼秒了
大致思路就是发现后手不败,因此考虑判断何时后手必胜
经过观察后会发现后手必胜时的树很有结构性质,大致为每个点儿子子树内的叶子编号形成公差相同、项数也相同的等差数列
这部分具体可以看题解,剩下的就是那个启发式合并维护下信息即可
#include <bits/stdc++.h>
constexpr int $n = 100005;
int pa[$n];
std::vector<int> out[$n], dep[$n], *hkr[$n];
void dfs(int cur, int d) {
dep[d].emplace_back(cur);
for(auto ch: out[cur]) if(ch != pa[cur]) {
pa[ch] = cur;
dfs(ch, d + 1);
}
}
void merge(std::vector<int> *&a, std::vector<int> *&b) {
if(!b) return;
if(!a) { a = b; return; }
if(a->size() < b->size()) std::swap(a, b);
for(auto ele: *b) a->emplace_back(ele);
b = nullptr;
}
bool work() {
int n, m; std::cin >> n >> m;
for(int i = 1; i <= n; ++i) {
out[i].clear(), dep[i].clear();
if(hkr[i]) { hkr[i] = nullptr; }
}
for(int i = 2, f, t; i <= n; ++i) {
std::cin >> f >> t;
out[f].push_back(t);
out[t].push_back(f);
}
pa[n] = 0;
dfs(n, 1);
int max_dep;
for(max_dep = 1; max_dep <= n; ++max_dep) {
int i = max_dep;
if(dep[i].size() == 0) break;
std::sort(dep[i].begin(), dep[i].end());
}
max_dep--;
for(int i = 1; i <= n; ++i) if(out[i].size() == 1) hkr[i] = new std::vector<int> {i};
for(max_dep--; max_dep > 0; max_dep--) {
for(auto node: dep[max_dep]) for(auto ch: out[node]) if(ch != pa[node])
merge(hkr[node], hkr[ch]);
bool flag = true;
int tot = 0;
for(auto node: dep[max_dep]) {
if(out[node].size() > 2) flag = false;
tot += hkr[node]->size();
}
if(flag) continue;
// std::cerr << "max_dep = " << max_dep << char(10);
// std::cerr << "dep[max_dep].size() = " << tot << char(10);
if(tot != m) return false;
int expected_size = m / dep[max_dep].size();
for(auto node: dep[max_dep]) {
std::sort(hkr[node]->begin(), hkr[node]->end());
if(hkr[node]->size() != expected_size) return false;
for(int i = 1; i < hkr[node]->size(); ++i) {
if(hkr[node]->at(i) - hkr[node]->at(i - 1) != dep[max_dep].size())
return false;
}
}
}
return true;
}
int main() {
std::ios::sync_with_stdio(false);
int T; std::cin >> T; while(T--)
std::cout << (work() ? "Doddle\n" : "Tie\n");
return 0;
}
E. Two in One
结论题
考虑以下经典结论,对于一个含有 \(a\) 个 \(0\) 和 \(b\) 个 \(1\) 的 01 串,对于任意的 \(x\in[0,a],y\in[0,b]\),均存在一个子串满足该子串恰好含有 \(x\) 个 \(0\) 和 \(y\) 个 \(1\)
有了这个结论后我们发现只要求出颜色序列中出现最多的两种颜色的个数,不难发现加入其它无关要素不影响上面结论的正确性
求最大的按位或值可以枚举其中一个的值然后贪心,代码十分好写
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
int t,n,x,bkt[N];
int main()
{
for (scanf("%d",&t);t;--t)
{
scanf("%d",&n);
for (RI i=1;i<=n;++i) bkt[i]=0;
for (RI i=1;i<=n;++i) scanf("%d",&x),++bkt[x];
int mx=0,smx=0;
for (RI i=1;i<=n;++i)
if (bkt[i]>mx) smx=mx,mx=bkt[i];
else if (bkt[i]>smx) smx=bkt[i];
int ans=0;
for (RI x=0;x<=mx;++x)
{
int y=0;
for (RI i=19;i>=0;--i)
{
if ((x>>i)&1) continue;
if ((y|(1<<i))<=smx) y|=(1<<i);
}
ans=max(ans,x|y);
}
printf("%d\n",ans);
}
return 0;
}
F. Gift
签到,不难发现答案只和删掉基环上的边后度数为 \(4\) 以及度数大于 \(4\) 的点个数有关,简单讨论下即可
#include<cstdio>
#include<iostream>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
int n,deg[N],pre[N],fa[N]; vector <int> v[N],path;
inline int getfa(CI x)
{
return fa[x]!=x?fa[x]=getfa(fa[x]):x;
}
inline void DFS(CI now,CI tar,vector <int>& path)
{
if (now==tar)
{
for (int x=now;x!=-1;x=pre[x]) path.push_back(x);
return;
}
for (auto to:v[now]) if (!pre[to]) pre[to]=now,DFS(to,tar,path);
}
int main()
{
scanf("%d",&n);
for (RI i=1;i<=n;++i) fa[i]=i;
for (RI i=1;i<=n;++i)
{
int x,y; scanf("%d%d",&x,&y);
++deg[x]; ++deg[y];
if (getfa(x)!=getfa(y))
{
fa[getfa(x)]=getfa(y);
v[x].push_back(y); v[y].push_back(x);
} else pre[x]=-1,DFS(x,y,path);
}
int d4=0,dg4=0; long long ans=0;
for (RI i=1;i<=n;++i)
if (deg[i]==4) ++d4; else if (deg[i]>4) ++dg4;
//for (auto x:path) printf("%d ",x); putchar('\n');
int sz=(int)path.size();
for (RI i=0;i<sz;++i)
{
int x=path[i],y=path[(i+1)%sz];
if (deg[x]==4) --d4; else if (deg[x]>4) --dg4;
if (deg[y]==4) --d4; else if (deg[y]>4) --dg4;
--deg[x]; --deg[y];
if (deg[x]==4) ++d4; else if (deg[x]>4) ++dg4;
if (deg[y]==4) ++d4; else if (deg[y]>4) ++dg4;
if (dg4==0) ans+=n-d4;
if (deg[x]==4) --d4; else if (deg[x]>4) --dg4;
if (deg[y]==4) --d4; else if (deg[y]>4) --dg4;
++deg[x]; ++deg[y];
if (deg[x]==4) ++d4; else if (deg[x]>4) ++dg4;
if (deg[y]==4) ++d4; else if (deg[y]>4) ++dg4;
}
return printf("%lld",ans),0;
}
G. Gene
神秘字符串当然是扔给徐神写,其中有涉及求 LCP 的部分为了方便因此写了哈希
值得一提的是徐神在写这个题的时候屡次发病,感觉万泉不诗人
#include <bits/stdc++.h>
using CI = const int;
const int mod1=998244353,mod2=1e9+7,mod3=1e9+9;
struct Hasher
{
int x,y;
inline Hasher(CI X=0,CI Y=0)
{
x=X; y=Y;
}
friend inline bool operator == (const Hasher& A,const Hasher& B)
{
return A.x==B.x&&A.y==B.y;
}
friend inline bool operator != (const Hasher& A,const Hasher& B)
{
return !(A==B);
}
friend inline Hasher operator + (const Hasher& A,const Hasher& B)
{
return Hasher((A.x+B.x)%mod1,(A.y+B.y)%mod2);
}
friend inline Hasher operator - (const Hasher& A,const Hasher& B)
{
return Hasher((A.x-B.x+mod1)%mod1,(A.y-B.y+mod2)%mod2);
}
friend inline Hasher operator * (const Hasher& A,const Hasher& B)
{
return Hasher(1LL*A.x*B.x%mod1,1LL*A.y*B.y%mod2);
}
} seed {31, 131}, ps[60005];
int n, q, m, k;
std::string s[305];
Hasher sh[305][60005], th[60005];
void make_hash(const std::string &s, Hasher *h) {
h[0] = {0, 0};
for(int i = 0; i < m; ++i) h[i + 1] = h[i] * seed + Hasher { s[i] - 'a' + 1, s[i] - 'a' + 1 };
return ;
}
Hasher query(Hasher *h, int l, int r) {
return h[r] - h[l - 1] * ps[r - l + 1];
}
int lcp(Hasher *a, int offset_a, Hasher *b, int offset_b) {
int res = 0;
int ca = offset_a, cb = offset_b;
for(int i = 1 << 19; i > 0; i >>= 1) {
int na = ca + i, nb = cb + i;
if(na > m + 1 || nb > m + 1
|| query(a, ca, na - 1) != query(b, cb, nb - 1))
continue;
ca = na, cb = nb;
}
return ca;
}
int main() {
std::ios::sync_with_stdio(false);
std::cin >> n >> q >> m >> k;
ps[0] = {1, 1};
for(int i = 1; i <= 60000; ++i) ps[i] = ps[i - 1] * seed;
for(int i = 0; i < n; ++i) {
std::cin >> s[i];
make_hash(s[i], sh[i]);
// for(int j = 1; j <= m; ++j) std::cerr << sh[i][j].x << char(j == m ? 10 : 32);
}
while(q--) {
std::string t; std::cin >> t; make_hash(t, th);
int ans = 0;
// std::cerr << query(th, 1, 1).x << ' ' << query(sh[2], 1, 1).x << char(10);
// for(int i = 1; i <= m; ++i) std::cerr << th[i].x << char(i == m ? 10 : 32);
for(int i = 0; i < n; ++i) {
int p = 1, mis = 0;
for(;;) {
p = lcp(sh[i], p, th, p);
if(p == m + 1) break;
// std::cerr << "Via << " << p << char(10);
mis += 1; p += 1;
if(mis > k) break;
}
// std::cerr << "Final << " << (p) << char(i == n ? 10 : 10);
if(mis <= k)
ans ++;
}
std::cout << ans << char(10);
}
return 0;
}
I. Indeterminate Equation
本来早过了,当时祁神写完 WA 了后就去上课了,然后我接手代码瞪了半天以及盘了好几遍逻辑后,发现是有个地方爆 long long
了,成功硬控我半小时
首先不难发现当 \(k>3\) 时,当 \(a>b>10^6\) 时 \(a^k-b^k\) 的值一定超过 \(10^{18}\),因此可以预处理出幂次后枚举+二分求解这种情形
对于 \(k=3\) 的情况,我们可以枚举 \(a-b\) 的值,固定该值后很容易列出关于 \(b\) 的一元二次方程,用求根公式判断是否有正整数根即可
#include<bits/stdc++.h>
using namespace std;
#define int __int128
const int N = 1e6+5;
const int K = 65;
int powe[K][N], len[K];
long long n, k;
long long solve() {
cin >> n >> k;
long long ans = 0;
if (k>3) {
for (int i=1; i<=len[k]; ++i) {
int pos = lower_bound(powe[k], powe[k]+len[k], powe[k][i]+n) - powe[k];
if (powe[k][pos] == powe[k][i]+n) ++ans;
}
} else {
for (int w=1; w<=(int)1e6; ++w) {
if (n%w!=0) continue;
int det = int(n)/w*12 - int(w)*w*3;
if (det < 0) continue;
int sqr = (int)sqrtl(det);
bool ok=false;
while (sqr*sqr <= det) {
if (sqr*sqr == det) {ok=true; break;}
++sqr;
}
while (sqr*sqr >= det) {
if (sqr*sqr == det) {ok=true; break;}
--sqr;
}
// printf("sqr=%lld ok=%d\n", (long long)sqr, ok);
if (!ok) continue;
int val1 = -3*w + sqr;
int val2 = -3*w - sqr;
if (val1>0 && val1%6==0) ++ans;
if (val2>0 && val2%6==0) ++ans;
}
}
return ans;
}
signed main() {
ios::sync_with_stdio(0); cin.tie(0);
len[1] = N-1;
for (int i=1; i<N; ++i) powe[1][i] = i;
for (int j=2; j<K; ++j) for (int i=1; i<N; ++i) {
powe[j][i] = powe[j-1][i]*i;
if (powe[j][i] - powe[j][i-1] > (int)1e18) {len[j]=i; break;}
}
signed t; cin >> t; while (t--) cout << solve() << '\n';
return 0;
}
K. Quad Kingdoms Chess
小清新数据结构题,但怎么又要哈希
对于某个人的序列,如果存在一个数,在其后面有严格大于它的数,那么这个数显然对对战没有影响
因此我们可以先给两个人对应的序列求出后缀最大值,只有值等于后缀最大值的位置才会被保留下来
考虑比较的过程其实就是在比保留下来的序列的字典序,因此平局当且仅当二者留下的序列相等
用线段树维护后缀信息,同时记录保留部分的哈希值用以比较,复杂度 \(O(n\log^2 n)\)
一个 trick 是由于最后保留下来的序列是单调不降的,因此可以不用传统的哈希来维护,直接给每个数随机一个值然后加起来即可
#include<cstdio>
#include<iostream>
#include<random>
#define RI register int
#define CI const int&
using namespace std;
typedef unsigned long long u64;
const int N=100005;
mt19937_64 rng(0X0D000721);
u64 rfx[N]; int q;
struct Segment_Tree
{
int n,arr[N],mx[N<<2]; u64 h[N<<2];
#define TN CI now,CI l,CI r
#define LS now<<1,l,mid
#define RS now<<1|1,mid+1,r
inline u64 find(CI rmx,TN)
{
if (l==r) return (mx[now]>=rmx?h[now]:0ull);
int mid=l+r>>1;
if (mx[now<<1|1]>=rmx) return h[now]-h[now<<1|1]+find(rmx,RS);
else return find(rmx,LS);
}
inline void pushup(TN)
{
mx[now]=max(mx[now<<1],mx[now<<1|1]);
int mid=l+r>>1;
h[now]=find(mx[now<<1|1],LS)+h[now<<1|1];
}
inline void build(TN)
{
if (l==r)
{
mx[now]=arr[l]; h[now]=rfx[arr[l]];
return;
}
int mid=l+r>>1; build(LS); build(RS); pushup(now,l,r);
}
inline void update(CI pos,CI mv,TN)
{
if (l==r)
{
mx[now]=mv; h[now]=rfx[mv];
return;
}
int mid=l+r>>1;
if (pos<=mid) update(pos,mv,LS); else update(pos,mv,RS); pushup(now,l,r);
}
inline u64 query(void)
{
return h[1];
}
#undef TN
#undef LS
#undef RS
}A,B;
int main()
{
for (RI i=1;i<=100000;++i) rfx[i]=rng();
scanf("%d",&A.n);
for (RI i=1;i<=A.n;++i) scanf("%d",&A.arr[i]);
scanf("%d",&B.n);
for (RI i=1;i<=B.n;++i) scanf("%d",&B.arr[i]);
A.build(1,1,A.n); B.build(1,1,B.n);
for (scanf("%d",&q);q;--q)
{
int tp,x,y; scanf("%d%d%d",&tp,&x,&y);
if (tp==1) A.update(x,y,1,1,A.n); else B.update(x,y,1,1,B.n);
puts(A.query()==B.query()?"YES":"NO");
}
return 0;
}
L. Rooted Tree
想到了就很简单的一个题
由于在一个深度为 \(x\) 的叶子节点处进行操作会贡献出 \(M\) 个深度为 \(x+1\) 的叶子节点,不难发现这里的贡献变化关于深度是线性的
因此如果我们维护出某个时刻树上所有叶子的深度的期望 \(E\),则它对答案的贡献为 \((E+1)\times M\),同时操作后 \(E\) 的值变化也很容易求出
实现时由于 \(K\) 较大,需要用离线求逆元,总复杂度 \(O(K)\)
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=1e7+5,mod=1e9+9;
int m,k,a[N],p[N],ip[N],inv[N];
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()
{
scanf("%d%d",&m,&k); int leaf=1,ans=0,E=0;
a[0]=1; for (RI i=1;i<=k;++i) a[i]=a[i-1]+m-1;
p[0]=1; for (RI i=1;i<=k;++i) p[i]=1LL*p[i-1]*a[i]%mod;
ip[k]=quick_pow(p[k]);
for (RI i=k-1;i>=1;--i) ip[i]=1LL*ip[i+1]*a[i+1]%mod;
for (RI i=1;i<=k;++i) inv[i]=1LL*ip[i]*p[i-1]%mod;
// for (RI i=1;i<=k;++i) printf("%d%c",a[i]," \n"[i==k]);
// for (RI i=1;i<=k;++i) printf("%d%c",inv[i]," \n"[i==k]);
for (RI i=1;i<=k;++i)
{
int tmp=1LL*(E+1)*m%mod;
(ans+=tmp)%=mod;
E=(1LL*E*(leaf-1)%mod+tmp)*inv[i]%mod;
// printf("E = %d\n",E);
leaf+=m-1;
}
return printf("%d",ans),0;
}
M. 3 Sum
被徐神一眼秒了的一个题,结果赛后看榜发现现场只过了 5 个队,震惊
首先如果我们将每个 \(a_i\) 都向 \(M\) 取模,那么 \(a_i+a_j+a_k\) 的值 \(\in[0,3M)\),直接分成 \(=0/M/2M\) 这些情况,用一个哈希来维护即可
快速求出某个高精度数对 \(10^K-1\) 取模的值可以用一个每 \(K\) 位分成一组,循环加法的 trick 来处理,这样代码就十分好写
由于我实现的时候取模后会出现 \(a_i=M\) 的情况,因此要加入 \(3M\) 这一情形,总复杂度 \(O(n^3+nK)\)
#include<cstdio>
#include<iostream>
#include<cstring>
#define RI register int
#define CI const int&
using namespace std;
const int N=20005;
const int mod1=998244353,mod2=1e9+7,mod3=1e9+9;
struct Hasher
{
int x,y,z;
inline Hasher(CI X=0,CI Y=0,CI Z=0)
{
x=X; y=Y; z=Z;
}
friend inline bool operator == (const Hasher& A,const Hasher& B)
{
return A.x==B.x&&A.y==B.y&&A.z==B.z;
}
friend inline Hasher operator + (const Hasher& A,const Hasher& B)
{
return Hasher((A.x+B.x)%mod1,(A.y+B.y)%mod2,(A.z+B.z)%mod3);
}
friend inline Hasher operator - (const Hasher& A,const Hasher& B)
{
return Hasher((A.x-B.x+mod1)%mod1,(A.y-B.y+mod2)%mod2,(A.z-B.z+mod3)%mod3);
}
friend inline Hasher operator * (const Hasher& A,const Hasher& B)
{
return Hasher(1LL*A.x*B.x%mod1,1LL*A.y*B.y%mod2,1LL*A.z*B.z%mod3);
}
}h[N],tar1,tar2,tar3,tar4;
const Hasher seed=Hasher(10,10,10);
int n,k,num[N*2]; char s[N];
int main()
{
scanf("%d%d",&n,&k);
for (RI l=1;l<=n;++l)
{
scanf("%s",s+1); int m=strlen(s+1);
for (RI i=1;i<=m;++i) num[i]=s[m-i+1]-'0';
int rm=(m+k-1)/k*k;
for (RI i=m+1;i<=rm;++i) num[i]=0;
for (RI i=rm/k-1;i>=1;--i)
{
int carry=0;
for (RI j=(i-1)*k+1;j<=i*k;++j)
{
num[j]+=num[j+k];
if (j+1<=i*k) num[j+1]+=num[j]/10; else carry=num[j]/10;
num[j]%=10;
}
num[(i-1)*k+1]+=carry;
for (RI j=(i-1)*k+1;j<=i*k;++j)
{
if (j+1<=i*k) num[j+1]+=num[j]/10;
num[j]%=10;
}
}
for (RI i=k;i>=1;--i)
h[l]=h[l]*seed+Hasher(num[i],num[i],num[i]);
// printf("h[%d] = %d\n",l,h[l].x);
}
for (RI i=1;i<=k;++i) tar2=tar2*seed+Hasher(9,9,9);
tar3=tar2*Hasher(2,2,2); tar4=tar2*Hasher(3,3,3);
tar1=Hasher(0,0,0); int ans=0;
for (RI a=1;a<=n;++a)
for (RI b=a;b<=n;++b)
for (RI c=b;c<=n;++c)
{
Hasher tmp=h[a]+h[b]+h[c];
ans+=(tmp==tar1||tmp==tar2||tmp==tar3||tmp==tar4);
}
return printf("%d",ans),0;
}
Postscript
这场最大的感受就是哈希含量有点超标了,贵校是哈希王国吗.jpg