D2T1
签。
#include <cstdio>
using namespace std;
int read(){/*...*/}
typedef long long ll;
void solve(){
ll n=read()-1,x=read();
ll y=x;
while(~y&1) y>>=1;
for(int i=1;i<n;++i) y<<=1;
if(x>y) y=x;
printf("%lld\n",y);
}
int main(){
int tc=read();
while(tc--) solve();
return 0;
}
D2T2
诈骗。发现翻转一个区间一定不会增加新的灵异区间,直接统计初始值即可。
#include <cstdio>
#include <unordered_map>
using namespace std;
int read(){/*...*/}
typedef long long ll;
const int N=100003;
int n,a[N];
int main(){
n=read();
unordered_map<int,int> mp;
long long res=0;
++mp[0];
for(int i=1;i<=n;++i)
res+=mp[a[i]=read()^a[i-1]]++;
printf("%lld\n",res);
return 0;
}
D1T1
首先由于 bfs 树分层,我们只需要对于跨层边考虑,发现限制相当于对于连着同一个节点一条树边和一条非树边,求出这两条边的另一个端点的 LCA,那么对于这个 LCA 分别往这两个端点延伸的树边有一个偏序的限制,拓扑排序即可。注意重边。
#include <cstdio>
#include <vector>
using namespace std;
int read(){/*...*/}
typedef long long ll;
const int N=200003,M=400003;
vector<int> vec[N];
int n,m;
int eu[M],ev[M];
namespace D{
const int N=::M;
const int M=1000003;
int hd[N],ver[M],nxt[M],tot;
int deg[N];
void add(int u,int v){
++deg[v];nxt[++tot]=hd[u];hd[u]=tot;ver[tot]=v;
}
int que[N],tl;
void topo(){
for(int i=1;i<=m;++i) if(!deg[i]) que[++tl]=i;
for(int pos=1;pos<=tl;++pos){
int u=que[pos];
printf("%d %d\n",eu[u],ev[u]);
for(int i=hd[u];i;i=nxt[i]){
int v=ver[i];
if(!--deg[v]) que[++tl]=v;
}
}
}
}
namespace G{
int hd[N],ver[M<<1],nxt[M<<1],tot=1;
void add(int u,int v){
nxt[++tot]=hd[u];hd[u]=tot;ver[tot]=v;
}
}
namespace T{
int hd[N],ver[N<<1],nxt[N<<1],tot;
void add(int u,int v){
nxt[++tot]=hd[u];hd[u]=tot;ver[tot]=v;
}
int de[N];
void dfs(int u){
vec[de[u]].emplace_back(u);
for(int i=hd[u];i;i=nxt[i]){
int v=ver[i];
de[v]=de[u]+1;
dfs(v);
}
}
}
using T::de;
int p[N],bel[N],f[18][N];
int main(){
n=read();m=read();
for(int i=1;i<=m;++i){
int u=eu[i]=read();
int v=ev[i]=read();
G::add(u,v);G::add(v,u);
}
int rt=0;
for(int i=1;i<=n;++i){
f[0][i]=p[i]=read();
if(!p[i]) rt=i;
else T::add(p[i],i);
}
for(int t=1;t<18;++t)
for(int i=1;i<=n;++i) f[t][i]=f[t-1][f[t-1][i]];
T::dfs(rt);
for(int d=1;d<=n;++d)
for(int x:vec[d]){
for(int i=G::hd[x];i;i=G::nxt[i]){
int v=G::ver[i];
if(v==p[x]) bel[x]=i>>1;
}
for(int i=G::hd[x];i;i=G::nxt[i]){
int v=G::ver[i];
if((i>>1)==bel[x]) continue;
if(de[v]==d-1){
int u=p[x];
for(int t=17;~t;--t)
if(f[t][u]!=f[t][v]){u=f[t][u];v=f[t][v];}
if(u!=v) D::add(bel[u],bel[v]);
else D::add(bel[x],i>>1);
}
}
}
D::topo();
return 0;
}
D1T2
\(m=n-1\) 为 Prufer 序板子,\(m=n\) 考虑建出仙人掌圆方树,由于只有一个方点相当于做 \(n+1\) 个点的树的情况。
\(m=n+1\) 依旧考虑建圆方树,对于有两个非平凡点双的情况相当于做 \(n+2\) 个点的树,但是要容斥去两个方点相邻的情况。
对于只有一个点双,发现点双形态一定是杏仁,简单计算即可。
#include <cstdio>
using namespace std;
int read(){/*...*/}
typedef long long ll;
const int N=100003,P=998244353;
int fac[N],fiv[N];
int qp(int a,int b=P-2){/*...*/}
int n,m;
int a[N];
namespace tree{
void solve(){
int sum=0;
int res=fac[n-2];
for(int i=1;i<=n;++i){
res=(ll)res*fiv[a[i]-1]%P;
sum+=a[i]-1;
}
if(sum!=n-2) puts("0");
else printf("%d\n",res);
}
}
namespace cir{
void solve(){
int sum=0;
int res=fac[n-1];
for(int i=1;i<=n;++i){
res=(ll)res*fiv[a[i]-1]%P;
sum+=a[i]-1;
}
if(sum>n-2) puts("0");
else{
if(res&1) res+=P;
res>>=1;
printf("%d\n",res);
}
}
}
int C(int n,int k){return (ll)fac[n]*fiv[k]%P*fiv[n-k]%P;}
namespace sol{
int ss;
int calc(){
int res=fac[n-1],sum=0;
for(int i=1;i<=n;++i){
res=(ll)res*fiv[a[i]-1]%P;
sum+=a[i]-1;
}
if(sum>n-1) return 0;
res=(ll)res*fiv[n-1-sum]%P;
ss=sum;
return res;
}
int tmp;
int sub1(){
int k=n-ss;
if(k<=3) return 0;
int coe=((ll)k*(k-1)>>1)%P;
coe=(ll)coe*(C(k,2)+P-3)%P*fac[k-2]%P;
coe=(ll)coe*qp(6)%P;
coe=(ll)coe*tmp%P;
return coe;
}
const int ivs=qp(4);
int sub2(){
int res=fac[n],sum=0;
for(int i=1;i<=n;++i){
res=(ll)res*fiv[a[i]-1]%P;
sum+=a[i]-1;
}
if(sum>n) return 0;
int cur=0;
for(int i=0;i<=n-sum;++i){
int coe=(ll)res*fiv[i]%P*fiv[n-sum-i]%P;
coe=(coe-(ll)tmp*C(n-sum,i))%P;
if(coe<0) coe+=P;
if(i>1&&n-sum-i>1)
cur=(cur+(ll)fac[i]*fac[n-sum-i]%P*coe%P*ivs)%P;
}
if(cur&1) cur+=P;
return cur>>1;
}
void solve(){
tmp=calc();
int res=sub1()+sub2();
if(res>=P) res-=P;
printf("%d\n",res);
}
}
int main(){
n=read();m=read();
for(int i=1;i<=n;++i) a[i]=read();
fac[0]=1;
for(int i=1;i<=n;++i) fac[i]=(ll)fac[i-1]*i%P;
fiv[n]=qp(fac[n]);
for(int i=n;i;--i) fiv[i-1]=(ll)fiv[i]*i%P;
if(m==n-1){
tree::solve();
return 0;
}
if(m==n){
cir::solve();
return 0;
}
sol::solve();
return 0;
}
D1T3
考虑如何形式化 Prufer 序的过程,相当于维护一个当前的叶子点集 \(S\) 和已经删除的点集 \(T\),每次移除叶子点集中最小的点到删除点集,然后看当前点是否能加入叶子点集。
由于这道题 Prufer 序整体形态不确定,我们可以先枚举初始的叶子点集(相当于在 DP 数组初始化时将这些点置为 1),然后每次遇到序列中的一个点决策其是否加入子序列中/是否加入叶子点集。只要最后 \(|S|=1,|T|=n-2\) 那么就说明中途的 Prufer 序其实合法。
现在考虑统计距离期望。我们把 DP 倒过来,每次对于每个不在 \(T\) 中的点 \(x\) 记录所有可能距离总和及其方案数。相当于设 \(DP_{S,T,x}\) 如果当前最小的叶子就是 \(x\),那么将它连上 \(a_t\) 的父亲时它的值要从 \(DP_{*,*,a_t}\) 处继承。
考虑优化掉 \(3^n\)。注意到 \(S,T\) 实际上的对数不是很多,事实上,结合 Prufer 序的 \(O(n)\) 转化法,我们发现 \(S\) 大概是 \(S\cup T\) 的一段后缀,但除了一个新加的点例外。所以它的实际总数是 \(O(2^n n^2)\) 的。然而这还不足以通过。
我们发现当前 \(S\) 中最小的点很快就要被删掉了,我们其实并不关心它具体是多少,只用知道它是不是 \(x\)。所以只记录一个 \(0/1\) 状态,总复杂度 \(O(2^n n^2 m)\)。
由于鬼畜的数据范围全程 vector,但是注意不要每次将数组滚动清空,否则会 T。
#include <cstdio>
#include <array>
#include <vector>
#include <algorithm>
#define fi first
#define se second
typedef long long ll;
using namespace std;
int read(){/*...*/}
typedef pair<int,int> pii;
typedef vector<int> vi;
typedef array<pii,2> ar;
typedef vector<ar> va;
typedef vector<va> vva;
typedef vector<vva> vvva;
const int P=1000000007;
void inc(int &x,int v){if((x+=v)>=P) x-=P;}
int n,m,_n,len;
int jump(int s,int x){
++x;
if(!(s>>x)) return _n;
return __builtin_ctz(s>>x)+x;
}
int qp(int a,int b=P-2){/*...*/}
int main(){
n=read();m=read();_n=n-1;len=(1<<_n);
vi arr(m),res(_n);
for(int i=0;i<m;++i) arr[m-i-1]=read()-1;
vvva f(len,vva(n,va(n)));
for(int i=0;i<n;++i){
f[len-1][_n][i][0]=pii(0,1);
f[len-1][_n][i][1]=pii(1,1);
}
vi fac(m+1),fiv(m+1);
fac[0]=1;
for(int i=1;i<=m;++i) fac[i]=(ll)fac[i-1]*i%P;
fiv[m]=qp(fac[m]);
for(int i=m;i;--i) fiv[i-1]=(ll)fiv[i]*i%P;
for(int x=0;x<m;++x){
for(int s=1;s<len;++s){
if(s>>arr[x]&1) continue;
for(int lim=0;lim<n;++lim)
if(lim==_n||(s>>lim&1))
for(int t=0;t<n;++t){
if(arr[x]<_n){
int _s=s|(1<<arr[x]);
int _lim=jump(_s,lim);
if(arr[x]<lim){
inc(f[s][lim][t][0].fi,f[_s][lim][t][arr[x]==t].fi);
inc(f[s][lim][t][0].se,f[_s][lim][t][arr[x]==t].se);
inc(f[s][lim][t][1].fi,f[_s][lim][arr[x]][1].fi);
inc(f[s][lim][t][1].se,f[_s][lim][arr[x]][1].se);
inc(f[s][lim][t][1].fi,f[_s][lim][arr[x]][1].se);
}
else{
inc(f[s][lim][t][0].fi,f[_s][_lim][t][lim==t].fi);
inc(f[s][lim][t][0].se,f[_s][_lim][t][lim==t].se);
inc(f[s][lim][t][1].fi,f[_s][_lim][arr[x]][lim==arr[x]].fi);
inc(f[s][lim][t][1].se,f[_s][_lim][arr[x]][lim==arr[x]].se);
inc(f[s][lim][t][1].fi,f[_s][_lim][arr[x]][lim==arr[x]].se);
}
}
if(lim<_n){
int _lim=jump(s,lim);
inc(f[s][lim][t][0].fi,f[s][_lim][t][lim==t].fi);
inc(f[s][lim][t][0].se,f[s][_lim][t][lim==t].se);
inc(f[s][lim][t][1].fi,f[s][_lim][arr[x]][lim==arr[x]].fi);
inc(f[s][lim][t][1].se,f[s][_lim][arr[x]][lim==arr[x]].se);
inc(f[s][lim][t][1].fi,f[s][_lim][arr[x]][lim==arr[x]].se);
}
}
}
}
int iv=(ll)fac[n-2]*fac[m-n+2]%P*fiv[m]%P;
for(int s=1;s<len;++s){
int lb=__builtin_ctz(s);
int p=jump(s,lb);
for(int i=0;i<_n;++i) inc(res[i],f[s][p][i][lb==i].fi);
}
for(int i=0;i<_n;++i) res[i]=(ll)res[i]*iv%P;
for(int i=0;i<_n;++i) printf("%d ",res[i]);
putchar('\n');
return 0;
}
D1T4
好题。对于一个有限的正整数集 \(S\),\(F(S)=\{x-y|x,y\in S,x>y\}\) 有一个大小下界 \(|S|-1\),而这个大小下界仅在等差数列处取到。
回顾题目中的博弈过程,我们发现轮到某一个固定的人操作时两个人数集大小的差是一样的。而当正在操作的人的数集大小比另一个人至少大二,那么显然这个人永远有办法给对方插入一个数。此时由于游戏必然有限轮内结束这个人必胜。
设先手 \(n\) 个数,后手 \(m\) 个数,当 \(m<n-1\) 时先手必胜,\(m>n\) 时后手必胜。
注意到 \(|F(S)|\) 取到 \(|S|-1\) 的条件是很苛刻的,绝大多数情况下我们可以认为 \(m=n\) 后手胜,\(m=n-1\) 先手胜。
考虑何时出现“以少胜多”的反例。结束局面时输家有一个 \(t+1\) 项的等差数列 \(\{a,a+d,a+2d,\dots,a+td\}\),而赢家正好有 \(\{d,2d,\dots,td\}\)
标签:typedef,YsOI2023,int,res,ll,long,read,小记 From: https://www.cnblogs.com/yyyyxh/p/YsOI2023.html