The 1st Universal Cup. Stage 5: Osijek
Preface
连着打了两天多校的本来想着今天找个国外场放松一下的,结果选到一个堪比毒瘤场的克罗地亚场直接被创飞了
最后磕磕绊绊过了 4.5 个题(A 题赛时被卡常了,赛后把取模删了改 long long
就卡过了),说好的放松呢
由于晚上还有 CF 而且我已经欠了好多博客没写了,剩下的题就先坑着了
A. And Xor Tree
首先徐神告诉我了一个把平方贡献拆掉的方法,即枚举两个二进制位 \(2^x,2^y\)
计算最后这两位均为 \(1\) 的路径数量,最后乘上 \(2^{x+y}\) 即可,证明的话把平方的式子拆开看一看贡献很容易知道
那么分别考虑三种运算符号的计算方式,与的情况非常显然只有这两位都为 \(1\) 的点之间能产生贡献,那么维护出两位全为 \(1\) 的点构成的联通块的大小即可计算贡献
或的情况和上面类似,只不过要容斥一下,用第一位为 \(1\) 的方案加上第二位为 \(1\) 的方案再减去两位全为一的方案
对于异或的情况想了半天发现只能 DP 了,令 \(f_{i,0/1,0/1}\) 表示 \(i\) 的子树到 \(i\) 路径上,第一/二位的异或值分别为多少时的路径条数
在每个点处用类似背包的形式合并下路径即可,总复杂度 \(O(n\times \log^2 a_i)\),但常数极大需要卡常
#include<cstdio>
#include<iostream>
#include<vector>
#include<cstring>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005,mod=998244353;
int n,m,a[N],ex[N],ey[N],fa[N],sz[N]; vector <int> v[N];
inline void inc(int& x,CI y)
{
if ((x+=y)>=mod) x-=mod;
}
inline int getfa(CI x)
{
return fa[x]!=x?fa[x]=getfa(fa[x]):x;
}
namespace AND
{
inline int solve(void)
{
RI i; int ret=0;
for (RI x=0;x<=m;++x) for (RI y=x;y<=m;++y)
{
for (i=1;i<=n;++i) fa[i]=i,sz[i]=1;
for (i=1;i<n;++i)
{
int u=ex[i],v=ey[i];
if (((a[u]>>x)&1)==0||((a[u]>>y)&1)==0) continue;
if (((a[v]>>x)&1)==0||((a[v]>>y)&1)==0) continue;
sz[getfa(u)]+=sz[getfa(v)];
fa[getfa(v)]=getfa(u);
}
long long cnt=0;
for (i=1;i<=n;++i) if (getfa(i)==i)
{
if (((a[i]>>x)&1)==0||((a[i]>>y)&1)==0) continue;
cnt+=1LL*sz[i]*sz[i];
}
cnt%=mod;
if (x==y) inc(ret,1LL*(1<<x)*(1<<y)%mod*cnt%mod);
else inc(ret,2LL*(1<<x)*(1<<y)%mod*cnt%mod);
}
return ret%mod;
}
};
namespace OR
{
inline int solve(void)
{
RI i; int ret=0;
for (RI x=0;x<=m;++x) for (RI y=x;y<=m;++y)
{
long long c1=1LL*n*n,c2=1LL*n*n,c3=1LL*n*n;
for (i=1;i<=n;++i) fa[i]=i,sz[i]=1;
for (i=1;i<n;++i)
{
int u=ex[i],v=ey[i];
if (((a[u]>>x)&1)==1||((a[v]>>x)&1)==1) continue;
sz[getfa(u)]+=sz[getfa(v)];
fa[getfa(v)]=getfa(u);
}
for (i=1;i<=n;++i) if (getfa(i)==i)
{
if (((a[i]>>x)&1)==1) continue;
c1-=1LL*sz[i]*sz[i];
}
c1%=mod;
for (i=1;i<=n;++i) fa[i]=i,sz[i]=1;
for (i=1;i<n;++i)
{
int u=ex[i],v=ey[i];
if (((a[u]>>y)&1)==1||((a[v]>>y)&1)==1) continue;
sz[getfa(u)]+=sz[getfa(v)];
fa[getfa(v)]=getfa(u);
}
for (i=1;i<=n;++i) if (getfa(i)==i)
{
if (((a[i]>>y)&1)==1) continue;
c2-=1LL*sz[i]*sz[i];
}
c2%=mod;
for (i=1;i<=n;++i) fa[i]=i,sz[i]=1;
for (i=1;i<n;++i)
{
int u=ex[i],v=ey[i];
if (((a[u]>>x)&1)==1||((a[u]>>y)&1)==1) continue;
if (((a[v]>>x)&1)==1||((a[v]>>y)&1)==1) continue;
sz[getfa(u)]+=sz[getfa(v)];
fa[getfa(v)]=getfa(u);
}
for (i=1;i<=n;++i) if (getfa(i)==i)
{
if (((a[i]>>x)&1)==1||((a[i]>>y)&1)==1) continue;
c3-=1LL*sz[i]*sz[i];
}
c3%=mod;
if (x==y) inc(ret,1LL*(1<<x)*(1<<y)%mod*(c1+c2-c3+mod)%mod);
else inc(ret,2LL*(1<<x)*(1<<y)%mod*(c1+c2-c3+mod)%mod);
}
return ret%mod;
}
};
namespace XOR
{
int f[N][2][2]; long long cnt;
inline void DP(CI x,CI y,CI now=1,CI fa=0)
{
RI p,q; int nx=(a[now]>>x)&1,ny=(a[now]>>y)&1;
if (nx&&ny) ++cnt;
memset(f[now],0,sizeof(f[now]));
for (auto to:v[now]) if (to!=fa) DP(x,y,to,now);
static int g[2][2]; memset(g,0,sizeof(g));
for (auto to:v[now]) if (to!=fa)
{
cnt+=2LL*f[to][1^nx][1^ny];
for (p=0;p<2;++p) for (q=0;q<2;++q)
cnt+=2LL*f[to][p][q]*g[1^p^nx][1^q^ny];
for (p=0;p<2;++p) for (q=0;q<2;++q)
f[now][p^nx][q^ny]+=f[to][p][q],g[p][q]+=f[to][p][q];
}
++f[now][nx][ny];
}
inline int solve(void)
{
int ret=0;
for (RI x=0;x<=m;++x) for (RI y=x;y<=m;++y)
{
cnt=0; DP(x,y); cnt%=mod;
if (x==y) inc(ret,1LL*(1<<x)*(1<<y)%mod*cnt%mod);
else inc(ret,2LL*(1<<x)*(1<<y)%mod*cnt%mod);
}
return ret;
}
};
int main()
{
RI i,j; for (scanf("%d",&n),i=1;i<=n;++i) scanf("%d",&a[i]),m=max(m,__lg(a[i]));
for (i=1;i<n;++i) scanf("%d%d",&ex[i],&ey[i]),v[ex[i]].push_back(ey[i]),v[ey[i]].push_back(ex[i]);
return printf("%d %d %d",AND::solve(),OR::solve(),XOR::solve()),0;
}
B. Balanced Permutations
题都没看,一眼寄
C. Cyclic Shifts
徐神和祁神后期一直在讨论这个题的做法,并且徐神决定后面把这个题补了,我就直接开摆了
D. Distinct Subsequences
感觉是个可做题,但我看了半天题解没太看懂在干啥,留着之后填坑吧
E. Epidemic Escape
又是个不可做题
F. Five Letter Warning
被祁神单切了,虽然中间我一直在否定祁神的做法,但结果就是我被狠狠打脸
大致思路就是给每个位置求出它前缀和后缀位置中每种字符的出现次数,考虑将第二个和第四个位置相同的贡献合并
把式子写出来发现可以把中间的系数拆分乘到两边去,但要注意实际实现时开不下这样的数组空间,因此可以枚举第一/第五个字符,再仅考虑相邻的相同字符
具体实现见代码,还需要特判第一/二/四/五个字符都相同的情况
#include<bits/stdc++.h>
using namespace std;
#define int long long
int MOD;
void add(int &x, int a){if ((x+=a)>=MOD) x-=MOD;}
const int N = 1e6+5;
string str;
int n, A[N], nxt[N], lst[100];
int pre[N], shuf[N], wt[N], wshuf[N], pshuf[N];
int ans = 0;
int C2(int n){
if (n<=1) return 0;
return n*(n-1)/2%MOD;
}
signed main(){
// freopen("F.out","w",stdout);
ios::sync_with_stdio(0); cin.tie(0);
cin >> str;
cin >> MOD;
n =str.length();
for (int i=0; i<n; ++i) A[i+1] = str[i]-32;
for (int i=1; i<=n; ++i){
if (lst[A[i]]>0) nxt[lst[A[i]]] = i;
lst[A[i]] = i;
}
for (int i=1; i<=94; ++i) if (lst[i]>0) nxt[lst[i]]=n+1;
for (int c=1; c<=94; ++c){
for (int i=1; i<=n; ++i) pre[i] = pre[i-1]+(A[i]==c);
for (int i=n; i>0; --i) shuf[i] = shuf[i+1]+(A[i]==c);
for (int i=n; i>0; --i) wt[i] = shuf[i]*(i-1);
for (int i=n; i>0; --i) pshuf[i] = pshuf[nxt[i]]+shuf[i];
for (int i=n; i>0; --i) wshuf[i] = wshuf[nxt[i]]+wt[i];
if (pre[n]==0) continue;
for (int i=1; i<=n; ++i){
add(ans, C2(pre[i-1])*C2(shuf[i+1])%MOD);
int j = nxt[i];
if (j>n) continue;
int L=0, R=0;
if (A[i]==c){
// L = pre[i]-1;
// R = (wshuf[j]-wt[j]) - (i*(pshuf[j]-shuf[j]));
// R %= MOD;
// continue;
}else{
L = pre[i];
R = wshuf[j] - i*pshuf[j];
R %= MOD;
}
add(ans, L*R%MOD);
}
}
cout << ans << '\n';
return 0;
}
G. Gridlandia
放在签到位置的构造题,但还是让我们队想了挺久的
首先要注意到一堵墙会连接两个格点,而格点的总数为 \((n+1)^2\),那么答案显然有上界 \(\lfloor\frac{(n+1)^2}{2}\rfloor\)
考虑如何构造出这个上界,不难想到从最边上一圈开始依次连接相邻的两个格点,由于这样一圈的格点数总是偶数(除了只剩下一个点的情况),因此总能构造到上界
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=1005;
int n; char c[N][N];
int main()
{
RI i,j; scanf("%d",&n);
for (i=1;i<=n;++i) for (j=1;j<=n;++j) c[i][j]='.';
if (n==1) return puts("U"),0;
for (RI a=1,b=n;a<=b;++a,--b)
{
if (a==b) { c[a][b-1]='R'; c[a][b+1]='L'; continue; }
if ((b-a+1)%2)
{
for (i=a;i<=b;++i)
{
c[i][a]=(i-a)&1?'.':'L';
c[i][b]=(i-a)&1?'.':'R';
}
for (j=a+1;j<=b-1;++j)
{
c[a][j]=(j-a)&1?'U':'.';
c[b][j]=(j-a)&1?'D':'.';
}
} else
{
for (i=a;i<=b-1;++i) c[i][a]=(i-a)&1?'.':'L';
for (i=a+1;i<=b;++i) c[i][b]=(i-a)&1?'R':'.';
for (j=a+1;j<=b;++j) c[a][j]=(j-a)&1?'U':'.';
for (j=a;j<=b-1;++j) c[b][j]=(j-a)&1?'.':'D';
}
}
for (i=1;i<=n;++i,putchar('\n')) for (j=1;j<=n;++j) putchar(c[i][j]);
return 0;
}
H. Holiday Regifting
不可做题,开摆
I. Julienne the Deck
手玩一下发现洗牌操作只会把原排列循环移位,因此\(1\) 的位置有 \(n\) 种,再乘上两个方向可知答案就是 \(2n\)
但是要特判 \(n\le 2\) 的情况,此时答案为 \(1\)
#include<bits/stdc++.h>
using namespace std;
#define int long long
signed main(){
ios::sync_with_stdio(0); cin.tie(0);
int n; cin >> n;
if (n<=2) cout << 1 << '\n';
else cout << n*2%998244353 << '\n';;
return 0;
}
J. Knight's Tour Redux
又是神秘构造题,但我们有暴力打表法,不用智慧也能过题
很容易写出一个暴力打表程序,会发现 \(n=6\) 时有如下的从 \((1,1)\) 出发遍历行列的方案:
这个方案有一个好处就是它的终止点 \((4,6)\) 可以一步跳到 \((7,7)\),这也就意味着我们把原问题缩小为了规模为 \(n-6\) 的子问题
此时只要再用暴力跑出 \(n=7,8,9,10,11\) 且从 \((1,1)\) 出发遍历的方案即可得到所有更大的构造方案
不过要注意 \(n=5\) 时有不从 \((1,1)\) 开始的遍历行列的方案,需要特殊处理
#include<cstdio>
#include<iostream>
#include<vector>
#include<utility>
#define RI register int
#define CI const int&
using namespace std;
typedef pair <int,int> pi;
int main()
{
int n; scanf("%d",&n);
if (n==1) return puts("POSSIBLE\n1 1"),0;
if (n<5) return puts("IMPOSSIBLE"),0;
vector <pi> A[15]; int s=0;
A[5]={{1,3},{4,4},{5,1},{2,2},{3,5}};
A[6]={{1,1},{2,4},{5,5},{6,2},{3,3},{4,6}};
A[7]={{1,1},{2,4},{3,7},{6,6},{7,3},{4,2},{5,5}};
A[8]={{1,1},{2,4},{3,7},{6,8},{7,5},{8,2},{5,3},{4,6}};
A[9]={{1,1},{2,4},{3,7},{6,8},{5,5},{4,2},{7,3},{8,6},{9,9}};
A[10]={{1,1},{2,4},{3,7},{4,10},{7,9},{6,6},{5,3},{8,2},{9,5},{10,8}};
A[11]={{1,1},{2,4},{3,7},{4,10},{7,11},{8,8},{11,9},{10,6},{9,3},{6,2},{5,5}};
puts("POSSIBLE");
while (n-s>=12)
{
for (auto [x,y]:A[6]) printf("%d %d\n",s+x,s+y); s+=6;
}
for (auto [x,y]:A[n-s]) printf("%d %d\n",s+x,s+y);
return 0;
}
Postscript
唉感觉现在要智慧没智慧,要科技没科技,这下该怎么办呢