ECFINAL2025游记 & 部分题解
前言
我发现我写的东西只能投游记分类才有可能被国内看到喵。所以主播决定在前面先写一篇游记镇楼。
闲话
1.31 以前
HdU 寒假时间肥肠晚,我们是 21 号考完试,也就是说相比其他放假早的学校,我们少了约半个月的训练时间。
然后嘎巴嘎巴 22 号开始训练,每天准时早上
不过在这里我要陈述我的罪孽:
-
首先是色欲之罪,由于恰逢寒假,宿舍里只有我一个人(划重点),再加上学期内宿舍环境并不允许我色色(注:此处特指 DLsite)。所以每天模拟赛一打完完大头就被小头控制,犯下了无数不可饶恕的罪行(此处应有蟹老板图)
-
其次是懒惰之罪。主播是在是 toooooooooooooooo lazy,通常是只补完赛场上没写出来的题就开始色色色色色色色色色了,并没有多少扩展训练。
-
再者是傲慢之罪。主播发现主播上学期并没有多少有效的训练时间。很重要的几点就是:主播报了个英语班(别问主播为什么会报,主播也不知道)每天占去约
1 个半小时的时间;主播每天写作业一周会占去几个小时的时间;主播没课的时候喜欢睡到9 点……以上种种,让主播认为主播已经足够努力了,忽视了相关的训练并实际上,对于 hDu 来说,多问 AI,认真写作业,上课听不听课问题不大。所以主播上课一直都在玩小手机,感觉玩手机也挺累的,所以主播感觉主播在(虚空)努力。
-
同时,主播由于太懒,没有一种全心全意投入训练的思想准备,主播认为这才是主要原因。
所以,主播的实力并没有提高多少。。。
不过,主播目前正在实施以下改革:
- 处于对电量和玩手机的综合考虑,主播准备选择两节课玩手机,剩下的课带电脑做题
- 早睡早起写题或者练英语(大概率完不成)
- 缩减 GAME 时间用来完成作业和写题
希望这学期能变得更强一点啊吧啊吧。
1.31
出发白金汉爵大酒店。不过我怎么没有遇到一场比赛是在 zJu 里面办的,想起逛逛 Zju 的校园 qaq。
酒店大大的,配有自助食堂(与其他酒店不同,这里是三餐都有提供,并且有各种各样的吃食),地下房间(第一次看见酒店在地下还设有房间),很多会议厅。其他地方到没有逛过。
果然,ll 因为地下房间便宜特地给我们定了地下房间,好像 300 左右一晚。房间虽然没有窗户但有窗帘(?),还有一个很哇塞的浴缸。
浴缸耶!我要泡澡!
吃完晚饭后到大厅逛了逛,看到了 Qingyu 和 Jiangly 一行人(我只认识两个)。太过社恐,只能一边假装有事情乱走一边偷偷看他们在干嘛 qaq,但也没有看出一个所以然来。。。
然后回去泡澡,泡澡好舒服啊 ~ 。可能泡太久了,泡完顿时感觉身体变虚了,还流了好多鼻血 \~。
早早睡觉。结果
2.1
早上醒来果然头巨 TM 痛。今天彻底放弃干任何事,希望好好休息。然后被队友拉去打华为挑战赛。基本坐着盯着队友写了三个小时,虽然一分木有。
晚上受不了了,直接打车逃回 hDU,早早睡了。
2.2
由于赶过去还有一个小时,所以
比赛开始。
开头看了 B, 由于头巨痛,给队友说了下题意和思路,让队友写了。
然后在想
然后继续想
然后看
然后两个队友调了近两个小时的
当时去看
然后最后一个小时队友在写
然后比赛就结束了。
嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤。
What can I say,man……我感觉我确实尽力了。。。。。。
队友觉得打的太差,一个滚回房间,一个跑去跟群友面基了,只留下我一个人痛苦的接受审判。
这几天到时没有看到 HZY,不过 HZY 已经要去 WorldFinal 了,嘤嘤嘤嘤嘤嘤嘤嘤嘤。
比赛完遇到了
- Macesuted 觉得 FD 强队太多了,并没有啥出线的希望,说打完这次就退役了。。。我还能说啥,说幸好我没考上 fD 吗。。。
- yyc 还是那么喜欢打崩铁和猎人游戏,他说他距离转图灵班差了零点零几的分数,希望这次拿个金看看能不能争取优惠,虽然最终被卡在了银牌第一名。。。
- cqh 感觉也是好起来了!比我们队高一题,说他在努力争取保研。
唉,上次相见还是半年以前,那么快就物是人非了。。。。。。在这里祝他们前程似锦吧。。。
接着是领奖环节,在颁发中学生奖的时候现是 hzg 匆忙上台一边带领一边寻找 wmy,所幸最后 wmy 也是成功站上奖台领奖了!(此处应有图但我手机里的图消失了,所以各位自行脑补即可)
最后一直待到了最后,看到冠军上去领奖。往常我可能会说一句什么“大丈夫当如是也”,但那刻心里只有说不出口的茫然和失落。
这次比赛一队最终还是没有出线,遥想我当年还是初中生的时候,就听过教练吹嘘 hzk 多么强劲,当时只会阿巴阿爸仰望,结果这三年时光竹篮打水一场空。唉。
感觉离我近的都没啥好事,还能咋滴,继续努力吧。努力会欺骗你,但努力至少会让自己心安一点。
题解
:::info{open} 较为简单的四个题(B,H,J,K)主播没有写题解,不会写的自己回去面壁思过。
G 题待补,可以上 qoj 参考官方题解。
不会的问题可以问 AI。请不要质疑 AI 的实力,AI 肯定比你强。
:::
A
:::info[题面]
给定
\Delta ABC ,直线l_1 经过线段AB ,直线l_2 经过点C ,且l_1||l_2 。定义 "好密铺" 为用全等于
\Delta ABC 的三角形(允许旋转,翻转,平移)恰好密铺l_1 和l_2 间的区域。且每个三角形同时和两个直线相接触。给定点
D ,问是否存在一种"好密铺",使得D 恰好在一个三角形的边上。
:::
- 先考虑"好密铺"中的三角形会如何摆放。
一个直觉猜想是三角形的三个点应该都在两条直线上,否则不大好做: :::info[证明]
假设
由于
那么依照图示,在确定完
接下来就应该讨论可能的摆放方案了:假设已经确定了
- 假设
A,D 在同一条直线,需要满足条件\angle DAC=\angle BCA,AC=AC ,所以摆放是固定的。
- 再考虑
C,D 在同一条直线的情况,显然需要\angle ACB=\frac{\pi}{2} 。没有其他限制。
那么,依据
- 第一种情况:
我们可以把上述密铺的构造看成线段
如果
上面的式子不大好算,我们可以转变视角:我们可以把
那么这样子就比较好算了。
设
- 第二种情况
由于上面的讨论,我们以两个三角形构建成的一个矩形为单元来看,矩形的对角线可能有两种情况。需要额外注意如果题目中的点
最后,
#include<bits/stdc++.h>
#define i128 __int128
#define ll long long
#define int long long
using namespace std;
struct Point
{
i128 x,y;Point(i128 x=0,i128 y=0):x(x),y(y){}
friend const Point operator+(const Point&lhs,const Point&rhs){return Point(lhs.x+rhs.x,lhs.y+rhs.y);}
friend const Point operator-(const Point&lhs,const Point&rhs){return Point(lhs.x-rhs.x,lhs.y-rhs.y);}
const Point operator*(const i128&rhs){return Point(x*rhs,y*rhs);}
void read(){int _x,_y;cin>>_x>>_y;x=_x,y=_y;}
};
i128 dot(Point A,Point B){return A.x*B.x+A.y*B.y;}
i128 cross(Point A,Point B){return A.x*B.y-A.y*B.x;}
i128 cross(Point A,Point B,Point C){return cross(B-A,C-A);}
Point A,B,C,D,V,P;
int check(Point X,Point Y,int k0)
{
i128 v1=cross(Y-X,D-X);
i128 v2=cross(Y-X,V);
if(v1%v2==0&&(!k0||(k0&&v1)))return 1;
return 0;
}
void solve()
{
int xd1,xd2,yd1,yd2;
A.read();B.read();C.read();cin>>xd1;cin.get();cin>>xd2>>yd1;cin.get();cin>>yd2;
i128 l=(i128)xd2*yd2/__gcd(xd2,yd2);
A=A*l,B=B*l,C=C*l,D.x=(i128)xd1*l/xd2,D.y=(i128)yd1*l/yd2;
V=B-A;
if(cross(V,D-A)==0){cout<<"Yes"<<endl;return;}
if(cross(V,D-C)==0){cout<<"Yes"<<endl;return;}
if((cross(V,D-A)>0)==(cross(V,D-C)>0)){cout<<<"No"<<endl;return;}
if(check(A,C,0)||check(B,C,0)){cout<<"Yes"<<endl;return;}
if(dot(C-A,B-A)==0&&check(A,C+B-A,1)){cout<<"Yes"<<endl;return;}
if(dot(C-B,A-B)==0&&check(B,C+A-B,1)){cout<<"Yes"<<endl;return;}
cout<<"No"<<endl;
}
signed main()
{
int _;cin>>_;while(_--)solve();
}
:::
C
:::info[题面]
给定长度为
其中
因此,我们转换思路,考虑一个构造,将
略微观察,对于某一个
那么假设
那么我们按照上述将
-
Q1.对于一个区间
[l,r] ,尝试化简式子找出求解的办法。 -
Q2. 对于区间
[l,r] ,求当v\in[l,r] 的时候的k 的区间。 -
Q3. 在上面的基础上快速计算答案。
Q1
我们考虑线段树上的一个区间
最后我们可以换一下
Q2
经过 Q1 的讨论,我们能够快速计算出给定任意一个
对于
\max 的贡献,我们对区间[l,r] 建出主席树,第i 棵树维护下标为i 的答案,对于线段树上的一个节点x ,对应区间为[a,b] ,贡献为\sum_{(i',k'),i'\le i,k'\in[a,b]}1 ,其中(i',k') 是 Q1 中的修改序列。(离散化一下)对于后面
\sum 的贡献,我们建立全局主席树,第i 棵树维护值为i 的时候,下标取[l,r] 时候的\sum_{j\in[l,r]}[a_j\le i] 的值。
那么我们考虑下标
由于
可以看出,共
Q3
接下来就要开始计算答案了,我们再看一下答案式子:
考虑对
- 对于
\sum_{(i',k')\in V_{l,r}}[k'\le k\And i\le i'] :
对区间
这样,当
- 对于
\sum_{j=i+1}^{k}[a_i<a_j\le lk_i]+(lk_i-a[i]))
后面
同理,我们开两个树状数组维护
综上,整道题可以在
代码写的很抽象,谨慎观看:
:::info[代码]
#include<bits/stdc++.h>
#define pii pair<int,int>
#define fi first
#define se second
#define mk make_pair
#define pb push_back
#define ll long long
#define mid ((L+R)>>1)
using namespace std;
const int N=1e5+100,M=N*300,inf=1e9+7;
int n;
int A[N];
ll ans[N];
vector<tuple<int,int,int>>aseg[N];
//aseg[i] 维护点 i 的修改区间 (x,l,r),表示线段树上的节点和对应区间
vector<int>g[N];
vector<pii>nopt[N<<2],q[N][2];
//nopt[k'] 维护 (x,i') 修改,x 是线段树节点
//q[k][0] 加入,q[k][1] 删除,维护 (x,i)
int ch[N];
BIT ca,cv;
namespace data_structure
{
struct Node//主席树节点
{
int val,ls,rs;Node(int val=0,int ls=0,int rs=0):val(val),ls(ls),rs(rs){}
}a[M];
int st[M],top,cnt;
int New(){return top?st[top--]:++cnt;}
void del(int&x){a[x]=Node();st[++top]=x;x=0;}
struct Persistent_Tree//所需要的主席树可以都改成单点修改,区间查询
{
void update(int&x,int y,int L,int R,int p,int v)
{
x=New();a[x]=a[y];
if(L==R){a[x].val+=v;return;}
if(p<=mid)update(a[x].ls,a[y].ls,L,mid,p,v);
else update(a[x].rs,a[y].rs,mid+1,R,p,v);
a[x].val=a[a[x].ls].val+a[a[x].rs].val;
}
int query(int x,int L,int R,int l,int r)
{
if(!x||l>r)return 0;
if(l<=L&&R<=r)return a[x].val;
int res=0;
if(l<=mid)res+=query(a[x].ls,L,mid,l,r);
if(r>mid)res+=query(a[x].rs,mid+1,R,l,r);
return res;
}
}PT;
struct PT1//主席树,以值为版本,rt[i] (l,r) 维护 l<=pos<=r,a[pos]<=i 的个数
{
int rt[N];
int query(int x,int y,int l,int r){return PT.query(rt[y],1,n,l,r)-PT.query(rt[x-1],1,n,l,r);}
}PT1;
struct Set_Tree//线段树维护二分图左部点集合
{
#define ls (x<<1)
#define rs (x<<1|1)
int val[N<<2],mn[N<<2],laz[N<<2];
//val[x] 表示 区间长度减去区间内点的数量,mn[x] 表示区间内集合点的最小编号,laz[x] 表示懒标记,vis[x]表示是否修改过
bitset<N<<2>vis;
queue<int>vt[N];//每个叶子节点的集合点编号可能不止一个,因此用队列维护
Set_Tree(){vis.reset();vis=~vis;}
void push(int x,int v){val[x]+=v,laz[x]+=v;vis[x]=1;}
void pushdown(int x){if(!laz[x])return;push(ls,laz[x]);push(rs,laz[x]);laz[x]=0;}
void pushup(int x){val[x]=min(val[ls],val[rs]);mn[x]=min(mn[ls],mn[rs]);}
void build(int x,int L,int R)//因为每个叶子节点的初值不好直接维护,因此改成重置被修改过节点的值
{
laz[x]=vis[x]=0;
if(L==R){val[x]=L,mn[x]=inf;while(!vt[L].empty())vt[L].pop();return;}
if(vis[ls])build(ls,L,mid);if(vis[rs])build(rs,mid+1,R);
pushup(x);
}
pii querypos(int x,int L,int R,int tag=0)//查询区间内val<0的最小位置,如果没有返回inf,如果tag=1则查询区间内mn最小的位置
{
vis[x]=1;
if(L==R){return mk(L,mn[x]);}
pushdown(x);
if(val[ls]<0)return querypos(ls,L,mid,tag);
if(val[rs]>=0)
{
if(!tag)return mk(inf,inf);
return mn[ls]<mn[rs]?querypos(ls,L,mid,tag):querypos(rs,mid+1,R,tag);
}
pii res=querypos(rs,mid+1,R,tag);
if(res.se>mn[ls])return querypos(ls,L,mid,tag|1);
return res;
}
int del(int x,int L,int R,int p)
{
vis[x]=1;
if(L==R)
{
int v=mn[x];
if(!vt[L].empty())mn[x]=vt[L].front(),vt[L].pop();else mn[x]=inf;
return v;
}
pushdown(x);
int res;
if(p<=mid)res=del(ls,L,mid,p);else res=del(rs,mid+1,R,p);
pushup(x);
return res;
}
void update(int x,int L,int R,int l,int r,int v)//区间修改
{
vis[x]=1;
if(l<=L&&R<=r)return push(x,v);
pushdown(x);
if(l<=mid)update(ls,L,mid,l,r,v);if(r>mid)update(rs,mid+1,R,l,r,v);
pushup(x);
}
void add(int x,int L,int R,int p,int v)//新加入一个集合内的点
{
vis[x]=1;
if(L==R){if(mn[x]==inf)mn[x]=v;else vt[L].push(v);return;}
pushdown(x);
if(p<=mid)add(ls,L,mid,p,v);else add(rs,mid+1,R,p,v);
pushup(x);
}
#undef ls
#undef rs
void build(){build(1,1,n);}
int querypos(){return querypos(1,1,n).fi;}
int del(int p){return del(1,1,n,p);}
void update(int l,int r,int v){update(1,1,n,l,r,v);}
void add(int p,int v){add(1,1,n,p,v);}
}ST;
struct BIT
{
#define lb(x) ((x)&(-(x)))
int sz;vector<int>vt;
void resize(int _sz){sz=_sz+1;vt.resize(sz);}
void clear(){vt.clear();}
void add(int x,int y){for(;x<sz;x+=lb(x))vt[x]+=y;}
int ask(int x){if(x<0)return 0;int t=0;for(;x;x-=lb(x))t+=vt[x];return t;}
int ask(int l,int r){return ask(r)-ask(l-1);}
};
struct MainTree//线段树维护被划分区间
{
#define ls (x<<1)
#define rs (x<<1|1)
vector<pii>rt[N<<2],opt[N<<2];
//rt 维护当前区间的 (i,\forall i<= i'(i',k')修改对应的主席树),并按 i 升序排序
BIT bt[N<<2][2];
int tl[N<<2],tr[N<<2];
//tl,tr 记录点 x 的区间左端点和右端点
//opt[x] 维护当前区间的 (v,p) 修改,表示时刻 p 有一个值为 v 的点被插入了。
int find(int x,int i){return (lower_bound(rt[x].begin(),rt[x].end(),mk(i,0)))->se;}
//对于下标 i,我要所有 i<=i' 的操作构成的主席树,那么即找到>=i 的第一个节点
int gpos(int x,int i){return (lower_bound(rt[x].begin(),rt[x].end(),mk(i,0))-rt[x].begin())+1;}
void build(int x,int L,int R)
{
tl[x]=L,tr[x]=R;
if(L==R)return;
build(ls,L,mid);build(rs,mid+1,R);
}
void getseg(int x,int L,int R,int v,int p)
{
if(L==R)return aseg[p].push_back({x,L,R}),void();
if(v<=mid)getseg(ls,L,mid,v,p),aseg[p].push_back({rs,mid+1,R});
else getseg(rs,mid+1,R,v,p);
}
void ins(int x,int L,int R,int v,int k)
{
if(v<=R&&v>L)opt[x].push_back({v,k});
if(L==R)return;
if(v-1<=mid)return ins(ls,L,mid,v,k);else ins(rs,mid+1,R,v,k);
}
void add(int x,int L,int R)
{
ST.build(1,1,n);
for(auto [v,k]:opt[x])
{
//a[time],time
ST.update(v-L,n,-1);//注意右边界改为 n 没有问题,因为操作范围不会超过 r,所以不会有 r+1~n 出现值<0而 l~r 的值全部大于等于 0 的情况。即 r+1~n 不会影响答案
ST.add(v-L,k);
int p=ST.val[1]<0?ST.querypos():inf;
if(p==inf)continue;
int i=ST.del(p);
ST.update(p,n,1);
g[i].push_back(k);
nopt[k].push_back({x,i});
}
bt[x][0].resize(opt[x].size()+3);bt[x][1].resize(opt[x].size()+3);
rt[x].push_back({n+1,0});
reverse(opt[x].begin(),opt[x].end());
//因为 (i',k') 按 i' 是要从大往小加,所以要倒序
for(auto [v,k]:opt[x])
{
rt[x].push_back({k,rt[x].back().se});
for(auto pos:g[k])PT.update(rt[x].back().se,rt[x].back().se,1,n,pos,1);
g[k].clear();
}
reverse(rt[x].begin(),rt[x].end());
if(L==R)return;
add(ls,L,mid);add(rs,mid+1,R);
}
#undef rs
#undef ls
}MT;
}
using namespace data_structure;
int search(int x,int y,int px,int py,int L,int R,int p,int vx,int vy)
/*
x,y 线段树区间内的主席树
px,py PT1 中的主席树
L,R 当前线段树区间
p 限制 k\ge p
vx,vy 当前的值
*/
{
auto check=[&](int x,int y,int px,int py)
{
return vx+a[x].val+a[px].val>=vy+a[y].val+a[py].val;
};
if(L==R)return check(x,y,px,py)?L:0;
if(mid<p||check(a[x].ls,a[y].ls,a[px].ls,a[py].ls))
return max(mid,search(a[x].rs,a[y].rs,a[px].rs,a[py].rs,mid+1,R,p,
vx+a[a[x].ls].val+a[a[px].ls].val,
vy+a[a[y].ls].val+a[a[py].ls].val));
return search(a[x].ls,a[y].ls,a[px].ls,a[py].ls,L,mid,p,vx,vy);
}
int cmp(int i,tuple<int,int,int>a,tuple<int,int,int>b)
{
auto&[x,l,r]=a;auto&[nx,nl,nr]=b;
int mx=MT.find(x,i),my=MT.find(nx,i);
return search(mx,my,PT1.rt[l],PT1.rt[nl],1,n,i,
A[i]-l-PT.query(mx,1,n,1,i)-PT.query(PT1.rt[l],1,n,1,i),
A[i]-nl-PT.query(my,1,n,1,i)-PT.query(PT1.rt[nl],1,n,1,i)
);
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin>>n;
MT.build(1,1,n);
for(int i=1;i<=n;i++)cin>>A[i],MT.getseg(1,1,n,A[i],i),MT.ins(1,1,n,A[i],i);
for(int i=1;i<=n;i++)g[A[i]].push_back(i);
for(int i=1;i<=n;i++)
{
PT1.rt[i]=PT1.rt[i-1];
for(auto x:g[i])PT.update(PT1.rt[i],PT1.rt[i],1,n,x,1);
g[i].clear();
}
MT.add(1,1,n);
for(int i=1;i<=n;i++)
{
struct sta//栈
{
tuple<int,int,int>v;
int l,r;
sta(tuple<int,int,int>v,int l,int r):v(v),l(l),r(r){}
};
stack<sta>s;
for(int j=0;j<aseg[i].size();j++)
{
int l=i;
while(!s.empty()&&(l=cmp(i,s.top().v,aseg[i][j])+1)<=s.top().l)s.pop();
if(!s.empty())s.top().r=l-1;
s.push(sta(aseg[i][j],l,n));
}
while(!s.empty())
{
int x=get<0>(s.top().v);
int kl=s.top().l,kr=s.top().r;s.pop();
if(kl<=kr)q[kl][0].push_back({x,i}),q[kr+1][1].push_back({x,i});
}
}
ll res=0;
ca.resize(n+2);cv.resize(n+2);
for(int k=1;k<=n;k++)
{
for(auto [x,r]:nopt[k])
{
res+=MT.bt[x][0].ask(1,MT.gpos(x,r));
MT.bt[x][1].add(MT.gpos(x,r),1);
}
for(int opt=1,v=-1;opt>=0;opt--,v*=-1)
for(auto [x,i]:q[k][opt])
{
res-=MT.tl[x]*v;
res+=MT.bt[x][1].ask(MT.gpos(x,i),MT.bt[x][1].vt.size()-1)*v;
MT.bt[x][0].add(MT.gpos(x,i),v);
res+=PT1.query(A[i]+1,MT.tl[x],i+1,k-1)*v;
cv.add(MT.tl[x],v);
}
res+=A[k];
ca.add(A[k],1);
res-=ca.ask(A[k],n);
res+=cv.ask(A[k],n);
ans[k]+=res;
}
for(int i=1;i<=n;i++)cout<<ans[i]<<" ";cout<<endl;
return 0;
}
:::
D
:::info[题面]
给定一个长度为
两人博弈,先手的每回合会将一个
定义游戏结束时的分数为:最长的只包含0的区间长度。先手想要最大化分数,后手想要最小化。若双方均采取最优策略,求最终的分数。
:::
显然,因为最终串的
:::info[略微证一下]
假设
显然,若先手在原串内按子游戏的方案操作,当后手不选择在这个子游戏内操作时,相当于先手凭空在这个子游戏内多出了一轮,这个子游戏内的分数不会变小;因此,后手也必须在这个子游戏内操作,那么最大分数至少是
同时,若先手选择一个原串的子游戏先操作,后手可以跟着在上一轮先手操作的子游戏内操作,这样每个子游戏的贡献不会超过原本只在子游戏内操作的贡献,即最大值至多是
综上,答案就是每个子游戏的分数的最大值。 :::
接下来我们讨论单个子游戏(记为
方法 1.
稍微模拟一下会发现,后手操作很像上文中提到的子游戏,但分割的两端之间的状态会有一些变化(可能一些问号已经被操作变成
进一步,我们发现,在第一轮中,假设先手操作了位置
:::info[证明]
根据之前的结论,
那么若
对于
T ,先手忽略T_{1..y} 的位置来操作。先手第一步走B 到达最优分数的构造的第一步。此后,若后手选择走在T_{y+1...|T|} 内,照搬B 游戏的策略;若后手走在T_{1...y} ,则先手随便。这样必然能构造出一个分数至少为B 游戏到达的分数的方案。
感性理解就是
既然我第一轮走
:::
然后,我们在考虑
同理,对于第
接下来,大概就能 dp 了。设
不失一般性的,假设在第一轮过后,先手选择了
我们紧接着来讨论第二轮先手的取值情况,
- 假设取
a_{k-2} 后的问号,答案就是a_{k-1}+a_{k-2}+a_{k}+2 - 假设取
a_{k-3} 后的问号,那么后手要么是操作a_{k-4} 后的问号,贡献变成a_{k-3}+a_{k-2}+a_{k-1}+a_{k}+3 ,要么后手操作a_{k-2} 后的问号。贡献为:结构为 a_1,a_2...a_{k-3}+a_{k-2}+1 的子游戏) 。(由上述证明,不需要考虑下标大于k-3 的子游戏的贡献)
进一步考虑的话,我们发现我们不用去考虑先手第二轮操作
:::info[证明]
先看怎么描述总体答案的:
对于这个操作,我们发现贡献就相当于先手第一轮取
分类讨论两类大小的取值
当
而当
由于
- 若我们假设后手选择
a_{k-2} 后的问号的贡献小于后手选择a_{k} 后问号的贡献,即第二轮后手选择a_{k-2} 后的问号的贡献会贡献到第一轮先手选择a_{p} 后的问号,后手选择a_{p-1} 后的问号的答案中。
可以推出:
同时,第一轮先手选择
回顾逻辑链,有:
这说明
- 若我们假设后手选择
a_{k} 后的问号的贡献小于等于后手选择a_{k-2} 后问号的贡献,同理可以推出:
即在第一轮先手选择
综上,我们不需要考虑这种情况下的贡献。 :::
- 同理,我们不需要考虑先手第二轮操作
a_{p} 后问号,后手操作a_{p-1} 后问号时的贡献。p\in[1,k-4]
:::info[证明]
显然,我们需要满足先手第二轮操作
我们假设在最优方案中第三轮先手操作
当我们第二轮先手操作
a_{q} 后的问号,后手操作a_{q+1} 后的问号,第三轮先手操作a_{p} 后的问号时,由于上面的条件限制,后手一定操作a_{p-1} 后的问号。即:
然后我们分后手的操作讨论。
- 假设后手操作
a_{q-1} 后的问号,这意味着:
根据逻辑链,有:
即我们当第二轮先手选择
- 假设后手操作
a_{q+1} 后的问号,这意味着:
同时,根据逻辑链,有:
这意味着,当第二轮先手选择
即第二轮选
(主播可能证烦了,有简单的证明可以和主播说,证明应该是没有大问题的)
:::warning
对于先手操作
:::info[代码]
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+100;
int n;
string s;
int pre[N],nxt[N];
int calc(vector<int>vt)
{
int res=0;
for(int i=0;i<vt.size();i++)res=max(res,vt[i]),pre[i]=nxt[i]=0;
for(int i=0;i<(int)vt.size()-1;i++)res=max(res,vt[i]+vt[i+1]+1);
for(int i=1;i<(int)vt.size()-2;i++)
{
pre[i]=vt[i+1]+vt[i]+vt[i-1]+2;
if(i>=2)pre[i]=max(pre[i],min(vt[i-2]+vt[i-1]+vt[i]+vt[i+1]+3,pre[i-2]));
}
for(int i=(int)vt.size()-3;i>=1;i--)
{
nxt[i]=vt[i]+vt[i+1]+vt[i+2]+2;
if(i+3<vt.size())nxt[i]=max(nxt[i],min(vt[i]+vt[i+1]+vt[i+2]+vt[i+3]+3,nxt[i+2]));
res=max(res,min(pre[i],nxt[i]));
}
return res;
}
void solve()
{
cin>>s;n=s.length();s=" "+s;
vector<int>vt;
int ans=0,res=0;
for(int i=1;i<=n;i++)
{
if(s[i]=='1'){vt.push_back(res),ans=max(ans,calc(vt)),res=0,vt.clear();}
else if(s[i]=='?'){vt.push_back(res);res=0;}
else res++;
}
vt.push_back(res),cout<<max(ans,calc(vt))<<endl;
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _;cin>>_;while(_--)solve();
return 0;
}
:::
方法 2.
闭眼考虑二分。即二分
显然先后手只关心问号,那么我们把问号抽出来构成一个序列,并预处理出来互不包含的区间,每个区间表示若先手取到了这里面的全部问号,那么就会形成一个长度大于等于
因此,我们的博弈转到了:先手是否存在一种操作,使得至少存在一个区间里的问号全被先手选中。
那么我们分类讨论区间的情况:
-
存在区间长度为
1 。先手直接取这个问号即可。 -
所有区间长度大于等于
3 ,我们可以做如下构造:
将所有问号按顺序两两配对,当先手选择一个问号时,后手选择一个与之匹配的问号。这样可以保证每对问号至少有一个位置为
1 。那么由于每个区间长度大于等于3 ,那么每个区间至少包含一对问号,即每个区间里面至少有一个问号被改为1 。
这样先手一定构造不出来合法答案。
- 如果存在区间长度等于
2 :
对于一个长度为
那么如果其他区间和这个区间有交,那么这个区间的一部分已经被确定了。
-
两个长为
2 的区间有交且交大小为1 。那么:先手可以操作交集的那个问号,后手只能在左右两个问号里选择一个。无论后手怎么选,左右两个问号总会剩一个,先手选择这个问号即可,即此种情况下先手必胜 -
一个区间长为
2 ,一个区间长为3 ,交集为1 :先手选择交集所在的那个问号,后手必然选择长度为2 区间的另一个问号,这时候长度为3 的区间相当于被缩短成了一个长为2 的区间。
由于上述讨论把所有情况都覆盖到了,上面的操作大概率是对的。
:::info[证明]
还是考虑问号匹配的构造,如果两个问号之间先手选择一个时后手必须选择另一个时,将这两个问号配对。同时,若一个区间包含一对匹配项,那么这个区间至少含有
如果不停执行上面的操作
如果推不出
(这样的修改操作对后手不利,若我们能说明在这种情况下先手仍然必输,则能说明在原局面下先手必输)
我们略微讨论下缩小区间的情况:
-
长度为
3 的区间不会被缩小。否则能够接着进行2 操作。 -
对于长度至少为
4 的区间,左端点和右端点都有可能已被匹配。长度至多减2 。
接着,我们按已匹配的问号将序列问号划分成若干段,且每一段全都是未被匹配的括号。修改完后的区间只存在于某一段中。同时,有:
-
某一段的长度不可能为
1 ,否则可以推出以他的前一个问号作为左端点的区间的长度最多只有3 ,会进行2 操作。 -
某一段的长度可能为
2 。且只有长度为2 的区间。 -
某一段的长度大于等于
3 时,区间长度至少为3 。(对于长度为4 的区间,长度最多被缩小1 ,因为不能覆盖整段长度)
那么,看成若干个子游戏,若先手选择某个子游戏进行操作,那么后手紧跟在先手也在这个子游戏进行操作,并且对每个子游戏分别构造策略:
-
如果先手选择了一个已被匹配的问号,后手直接选择另一个匹配项即可。
-
若某段的长度为
2 ,先手选问好时后手选择另一个问号,由于只有长度为2 的区间,先手必输。 -
若某段的长度大于等于
3 ,由于所有区间长度大于等于3 ,采用之前所说的构造,先手必输。
综上,先手必输,得证。 :::
那么问题就相当于对于先手,对于长度为
那么再分析一下,除去一些直接判断的情况(存在长度为
那么,我们从左往右扫描,对于当前长度为
:::info[代码]
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+100;
int n;
string s;
int id[N],nxt[N],pre[N];
int calc(int l,int r,int i,int j){return min(r,nxt[j]-1)-max(l,pre[i]+1)+1;}
int check(int l,int r,int x,int st)
{
if(r-l+1<x)return 0;
if(st>r)return 1;
int nowl=0,nowr=0;
for(int i=st,j=i;i<=r;i=nxt[i])
{
while(j<i)j=nxt[j];
while(j<=r&&calc(l,r,i,j)<x)j=nxt[j];
if(j>r)break;
int tl=id[i],tr=id[j];
if(tl>=tr)return 1;
else if(tr-tl+1==2)
{
if(nowr==tl)return 1;
else nowl=tl,nowr=tr;
}
else if(tr-tl+1==3)
{
if(nowr==tl)nowl=tl+1,nowr=tr;
}
}
return 0;
}
int check(int x)
{
int l=0,st=0;
for(int i=1;i<=n;i++)
if(s[i]=='1')
{
while(st<l+1)st=nxt[st];
if(check(l+1,i-1,x,st))return 1;
l=i;
}
while(st<l+1)st=nxt[st];
return check(l+1,n,x,st);
}
void solve()
{
cin>>s;n=s.length();s=" "+s;
int p=0,cnt=0;
for(int i=0;i<=n+1;i++)id[i]=nxt[i]=pre[i]=0;
for(int i=1;i<=n;i++)if(s[i]=='?')id[i]=++cnt,nxt[p]=i,pre[i]=p,p=i;
nxt[p]=n+1,pre[n+1]=p;
int l=0,r=n,ans=0;
while(l<=r)
{
int mid=(l+r)>>1;
if(check(mid)){ans=mid,l=mid+1;}
else r=mid-1;
}
cout<<ans<<endl;
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _;cin>>_;while(_--)solve();
return 0;
}
/*
1
00?00?100?0
*/
:::
E
:::info[题面]
-
青鱼有
n 个车站,第i 个车站有等级a_i ,满足1\le ai\le t 。 -
青鱼开通了
m 种不同的列车,第i 种有参数x_i,y_i,p_i (1\le x_i,y_i \le k ),表示在1∼p_i 站中停靠等级\ge a_i 的车站,在p_{i}+1∼n 站中停靠等级\ge y_i 的车站。 -
定义两个站直接可达当且仅当存在一种列车同时停靠两个站。给定
p_{1..m} ,x_{1..m} ,求赋值y_{1,2..m} (1\le y_i\le k ) 的方案数使得任意两个等级相同的站均可直接到达。 -
n,m\le 10^3,k\le 500,1\le a_i,x_i,y_i\le k :::
我们需要保证对于每种车站
那么我们记等级为
再来考察单个列车
- 列车等级
x 大于等于\max(x_i,y_i) -
x\ge x_i \And rm_x\le p -
x\ge y_i \And lm_x>p
对于第一种讨论,我们只需要知道
满足第二种讨论的车站等级是固定的,这部分我们可以先预处理掉。
对于第三种讨论,我们可以看成一个前缀的形式,即对于某个等级
那么,接下来的思路就清晰了,我们先用容斥(或者 DP 额外记一维)把第一种讨论的限制去掉:
假设我们要求
\min_{所有列车}(\max(x_i,y_i))=v 的答案。那么在去掉所有等级大于等于v 的车站后,写成如下形式:\min(\max(x_i,y_i))\ge v 的答案减去\min(\max(x_i,y_i))> v 的答案。
:::warning[注意]
不能直接化成
然后就是对第三种讨论技术了,这是个 naive 的 DP,我是这么实现的:
把车站按
p 排序,并预先处理mn_i 表示\forall i,lm_i\le i,\min(i) ,并设dp_{i,j} 表示从前往后 DP 到了第i 个列车,前i 个列车的y 的最小值为j 的方案数。那么对于一步转移
dp_{i,j}\to dp_{i+1,\min(j,k)} ,我们要保证\forall q,lm_q\in [p_i,p_{i+1}),q\le j 。由于
dp_{i,j} 合法,那么可以推出前i 俩列车的最小值小于等于\forall q,lm_q\le p_i,\min(q) ,即j\le mn_{p_i} 。合起来就是限制:
j\le mn_{p_{i+1}-1} 。实现的时候可以在枚举k 的时候直接把k 的限制也判掉。另外,还要满足\min(x_{i+1},k)\ge v 或\min(x_{i+1},k)>v 。显然满足条件的
k 可以连续的分为两类不同的贡献,前缀和和差分优化即可。
那么总的复杂度就是:
:::info[代码]
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod=998244353,N=1e3+10,M=510;
int n,m,k;
int a[N],pl[N],pr[N];
//pl[i] 和 pr[i] 表示等级为 i 的车站的最左端和最右端位置,没有为 0
vector<int>g[N];
int dp[N][M],mn[N],t[N];
struct Node{int p,x;Node(int p=0,int x=0):p(p),x(x){}}p[N];
int power(int x,int y,int t=1){for(;y;y>>=1,x=x*x%mod)if(y&1)t=t*x%mod;return t;}
int calc(int k0,int lim)
//只考虑 <=k0 的车站,要求 \min(\max(x_i,y_i))>=lim
{
for(int i=0;i<=n;i++)mn[i]=lim;
for(int i=1;i<=k0;i++)if(pl[i])mn[pl[i]]=min(mn[pl[i]],i);
for(int i=1;i<=n;i++)mn[i]=min(mn[i],mn[i-1]);
if(mn[p[1].p]<lim)return 0;//1~p[1].p-1 并没有列车覆盖
for(int i=0;i<=m;i++)for(int j=1;j<=lim;j++)dp[i][j]=0;
dp[0][lim]=1;
// for(int i=0;i<m;i++)for(int j=1;j<=lim;j++)if(dp[i][j])
// for(int nxt=(p[i+1].x>=lim?1:lim);nxt<=k;nxt++)
// if(mn[p[i+2].p]>=min(nxt,j))
// (dp[i+1][min(nxt,j)]+=dp[i][j])%=mod;
//O(nk^2) 暴力
for(int i=0;i<m;i++)
{
for(int j=1;j<=lim;j++)t[j]=0;//t 记录差分数组
for(int j=1;j<=lim;j++)if(dp[i][j])
{
int l=1,r=k;
if(p[i+1].x<lim)l=lim;
if(j>mn[p[i+2].p])
{
r=min(r,mn[p[i+2].p]);
if(l<=r)(t[l]+=dp[i][j])%=mod,(t[r+1]+=mod-dp[i][j])%=mod;
}
else
{
(dp[i+1][j]+=(r-max(l,j)+1)*dp[i][j])%=mod;
if(l<j)(t[l]+=dp[i][j])%=mod,(t[j]+=mod-dp[i][j])%=mod;
}
}
for(int j=1;j<=lim;j++)(t[j]+=t[j-1])%=mod,(dp[i+1][j]+=t[j])%=mod;
}
int res=0;for(int j=1;j<=lim;j++)(res+=dp[m][j])%=mod;
// cout<<k0<<' '<<lim<<" "<<res<<endl;
return res;
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin>>n>>m>>k;
for(int i=1;i<=n;i++)
{
cin>>a[i];pr[a[i]]=i;
if(!pl[a[i]])pl[a[i]]=i;
}
for(int i=1;i<=m;i++)cin>>p[i].p>>p[i].x;
sort(p+1,p+m+1,[&](const Node&x,const Node&y){return x.p<y.p;});
for(int i=1;i<=k;i++)if(pr[i])g[pr[i]].push_back(i);
int mn=k+1;for(int i=n,j=m;i>=1;i--)//第二类讨论
{
while(j>=1&&p[j].p>=i){mn=min(mn,p[j].x);j--;}
for(auto x:g[i]){if(x>=mn||pl[x]==pr[x])pl[x]=pr[x]=0;}
g[i].clear();
}
int ans=0;
p[m+1].p=n;
for(int i=1;i<=k;i++)ans=(ans+mod+calc(i-1,i)-calc(i-1,i+1))%mod;
cout<<ans<<endl;
return 0;
}
:::
F
:::info[题面]
记
-
-
\forall [l_0,r_0]\in S,[l_1,r_1]\in S,l_0\le l_1<r_0\le r_1$,必须满足 $[l_0,l_1]\in S$ 且 $[r_0,r_1] \in S
对于所有青的集合
:::
假设我们已经会了这道题,我们来看看这道题怎么做。
性质
首先来发掘一些特殊性质:
- 首先
[l_0,r_0],[l_1,r_1] 的长度大于1 。 - 若
l_0=l_1 ,可以推出[r_0,r_1] \in S
- 若
l_0<l_1<r_0\le r_1 ,根据[l_0,r_0] (区间1 ) 和[l_1,r_1] (区间2 ) 可以推出[l_0,l_1] (区间3 );根据区间1 和区间3 可以推出[l_1,r_0] (区间4 )。
这种情况给了我们一些提示:
- 记
S 中所有左端点为x 的区间的右端点的最小值为R_x ,那么不存在区间[l,r] 满足x<l<R_x\le r ,否则根据上图可以推出:[x,l] \in S ,则由R_x\le l 矛盾。这说明了我们可以把左端点大于等于x 的区间分成:- 左端点在
x ; - 区间被
[x+1,R_x-1] 包含 - 区间被
[R_x,n] 包含
- 左端点在
这说明如果我们能处理左端点在
-
对于任意一个在
S 的区间[x,r] ,我们用[x,r] 和[x,R_x] 可以推出区间[R_x,r] 。那么我们记Sr_x 表示所有左端点为x 的区间的右端点构成的集合,那么应该可以推出以下联系:Sr_x/\{x\}\subseteq Sr_{R_x} 。 -
我们发现似乎对于
S_x 的限制还不够。回顾上文,我们还剩下r_0=r_1 的情况未讨论,那么:
区间
假设我们已经确定了所有被
:::info[证明]
首先,一个合法的左端点为
显然,我们只需要关注新加的
首先,由于
因此假设成立。 :::
DP
虽然推性质有点难。不过之后我们可以设计出一个复杂度较高的 DP 了。
我们记
(长度为
再看递推式子:
- 第一条式子:
|R_0|=1 时并不会对左端点大于0 的区间组合,因此可以看成去掉0 这个点后的答案 - 第二条式子:考虑枚举
R_x ,我们把[0,R_x-1] 看成一个左端点为0 的区间只有[0,0] 的一个子问题,答案即F_{R_x,0} 。我们把[R_x,i-1] 看成另一个子问题,不过要求|Sr_{R_x}|\ge |S_x\{x\}| ,那么就是\sum_{l\ge j-1}f_{i-R_x,l} 。那么我们枚举k=R_x 即可。
答案即
化简
接下来就该数学大手子发力了。
我们考虑把
那么重写上面的式子:
(递推时要补上
下面给出推式子的两种方法:
:::info[方法1]
既然涉及到
写成比值的形式,可以发现有相邻项比值的关系,即:
那么设
取
可以看出,化成了类似连分数的形式。我们采用一个弱智小技巧,设
那么根据解析式我们可以较轻松的求出
那么可以化出等式:
那么,设
那么,我们设函数
不过,为了方便讲解,我们统一一下形式:设
再带入
::: :::info[方法2]
由于递推方程式中带个
考虑
我们发现,主播主播,这也不能啊?但题解就是这么写的呀?。好吧,其实不需要这么麻烦,我们可以肉眼看出
那我们设
由于我们要求:
因此,我们设
我们可以思考
从
(1,0) 开始(因为不存在H_j(z) 有0 次项,只有H_0(z) 有一次项,其中(i,j) 表示走到了[z^i]H_j(z) 这一项上),每次向右走一步,最多向上走一步,可以向下走任意步(不能碰到y=0 的位置),。一个路径((x_1=1,y_1)\to (x_2=2,y_2)...(x_m=m,y_m) )的权值为q^{\sum_{i=1}^{m}y_m} 。那么[z^m]P(z) 就是所有从(1,0) 走到(m,j) (j\ge 1 ) 的路径的权值之和。
对于一条路径,考虑抽出所有满足
那么我们尝试用
因为
那么设
只要完成求解就能完成主人的任务了!
求解 P(x)=\frac{x}{1-P(qx)}
下面给出三种求解
:::info[方法1:全在线卷积]
变形式子
设
p_i=[x^{i}]P(x),g_{i}=q^{i}p_i ,P(0)=0\to q_0=p_0=0,p_1=1 ,式子为:p_i=\sum_{i=0}^{n}p_ig_{n-i}=\sum_{i=1}^{n-1}p_ig_{n-i},i>1 我们用函数
solve(l,r) 表示已经求解了p_{0...l-1} 现在要求解p_{l...r} ,并且设f_i 表示:f_i=\sum_{j=1}^{i-1}[j<l\And i-j<l]p_jg_{n-j} ,即所有下标小于l 的p 和g 对p_{l...r} 的贡献的和。
边界情况:
l=r\to p_i=f_i,g_i=qp_i 。那么设
mid=\lfloor \frac{l+r}{2}\rfloor ,我们先调用solve(l,mid) 求出p_{l...mid} ,再考虑更新f 数组并递归到solve(mid+1,r) 。即:i\in[mid+1,r],f_{i}=\sum_{j=l}^{mid}[i-j\le mid]p_jg_{i-j}+\\ \sum_{j=l}^{mid }[i-j<l]g_{j}p_{i-j} 即我们构造如下序列进行卷积并求和即可:
可以看出,上述序列的大小均为 $O(r-l)$,因此可以套用主定理进行分析,复杂度 $O(n\log^2n)$。 :::
::::info[方法 2:神秘数学技巧]
- 方法
2 :实际上P(x) 是一个连分数,如果你会连分数,参考他的渐进误差,会想到设比值的方法求解,稍微调整一下设的项,你会发现可以使用一个有趣的式子求出P(x) :设F(x) 满足P(x)=\frac{xF(qx)}{F(x)} 。
那么设
那么如果
- 如果
q=1 ,有:
有两个解,我们带一下特殊值:
:::info[方法1:牛顿二项式展开]
:::
:::info[方法2:求导高手]
设
两边求导,可得:
我们设
(注意手算
回带到
同理,当
由于
同理,可以推得:
:::
那么,我们能手动推出来
-
q=1$:$p_0=0,p_1=1,p_i=\frac{4i-6}{i}p_{i-1} -
q=-1$:$p_0=0,p_1=1,p_2=-1,p_i=\frac{12-4i}{i}p_{i-2}
::::
::::info[方法 3:连分数递推]
观看 OI wiki 上关于连分数的前一部分教程,用于主播在学多项式,我们只讨论形式幂级数下的连分数。那就开始今天的红色沙漠连分数大学习吧。
:::info[基础知识]
具体来说,如果一个形式幂级数
一般记为:
其中
| 显然, |
序列 | 递推公式 ( |
初始条件 (通常设定) |
|---|---|---|---|
| 分子 |
|||
| 分母 |
证明如下:
-
n=0$:$C_0=a_0$,$\frac{A_0}{B_0}=a_0 -
n=1$:$C_1=\frac{a_0a_1+b_1}{a_1}$,$A_1=a_0a_1+b_1$,$B_1=a_1$,$\frac{A_1}{B_1}=\frac{a_0a_1+b_1}{a_1} -
假设对于项数小于等于
k 的连分数,公式都成立,即C_k = [a_0; (b_1, a_1), \dots, (b_k, a_k)] = \frac{A_k}{B_k} = \frac{a_k A_{k-1} + b_k A_{k-2}}{a_k B_{k-1} + b_k B_{k-2}} 现要证明
C_{k+1} 也可以用上述式子表示。
现在我们看
那么可以看作:
,即
综上,归纳成立。
同时,为了进行误差分析,即为了考察
:::
那么,根据
那么有:
并且根据递推式子我们可以反推复合条件的
我们发现在一类特殊的形式幂级数下,我们可以采用
|C_n-C_{n-1}|=\frac{|\prod_{i=1}^{n}b_i|}{B_{n}B_{n-1}}=\frac{q^{\frac{n(n-1)}{2}}x^{n}}{B_{n}B_{n-1}} 根据上面的递推式,取
[x^0] 有:[x^0]B_i=[x^0]B_{i-1} ,因此,[x^0]B_i=1 。即B_i 存在乘法逆元,即B_i=1+O(x) 。因此:原式=\frac{q^{\frac{n(n-1)}{2}}x^{n}}{B_{n}B_{n-1}}=O(x^{n}) 即
C_n\equiv C_{n-1}\pmod {x^{n}} ,因为是在形式幂级数下分析,有:\lim_{n\to +\infin}C_n=P(x) ,即可以推出C_{n}\equiv P(x)\pmod x^{n} 。注:如果你知识扎实的话,可以发现
[x^0]F\not =0 是F 存在乘法逆元的充要条件,并且除上F 可以看作乘上F 的乘法逆元,并且可以如此构造出来:[x^0]F^{-1}=1\\ \sum_{j=0}^{i}[x^j]F_j[x^{i-j}]F^{-1}=0,i>0\\ \to [x^i]F^{-1}=-\frac{1}{[x^0]F}\sum_{j=0}^{i-1}[x^j]F^{-1}[x^{i-j}]F 那么你自然就可以发现:两个多项式相除
\frac{A}{B} ,如果B 存在逆元,那么\frac{A}{B} 的最低次幂和A 的最低次幂相同。
综上,我们只需要求解
那么设
设
而实际上对于矩阵内的每一项元素,都有如下关系:
那么
求解复合逆
算出
实现
下面给出根据三种方式求解
:::info[方法一]
const int mod=998244353;
Z Inv(Z x){return power(x,mod-2);}
int n,q;
Poly p,f,g;
void solve(int l,int r)
{
if(l==r)
{
p[l]=f[l],g[l]=power(Z(q),l)*p[l];
return;
}
int mid=(l+r)>>1;
solve(l,mid);
Poly A,B,C;A.resize(mid-l+1),B.resize(min(mid,r-l)+1);
for(int i=0;i<A.size();i++)A[i]=p[i+l];
for(int i=0;i<B.size();i++)B[i]=g[i];
C=A*B;
for(int i=mid+1;i-l<C.size()&&i<=r;i++)f[i]+=C[i-(l)];
A.resize(mid-l+1),B.resize(min(l-1,r-l)+1);
for(int i=0;i<A.size();i++)A[i]=g[i+l];
for(int i=0;i<B.size();i++)B[i]=p[i];
C=A*B;
// cout<<l<<" "<<r<<' '<<A.size()<<' '<<B.size()<<endl;
for(int i=mid+1;i-l<C.size()&&i<=r;i++)f[i]+=C[i-(l)];
solve(mid+1,r);
}
signed main()
{
cin>>n>>q;n+=2;
p.resize(n+1);f.resize(n+1);g.resize(n+1);
f[0]=0,f[1]=1,p[0]=0,p[1]=1,g[0]=0,g[1]=q;
solve(1,n);
p[0]+=1;
Z ans=(p.pow(n,n))[n-1]*Inv(n);
cout<<ans*power((Z)q,n-1)<<endl;
return 0;
}
:::
:::info[方法二]
const int mod=998244353;
Z Inv(Z x){return power(x,mod-2);}
int n,q;
Poly p,f,g;
signed main()
{
cin>>n>>q;n+=2;
p.resize(n+1);//
if(q==1)
{
p[0]=0;p[1]=1;for(int i=2;i<=n;i++)p[i]=Z(4*i-6)*Inv(i)*p[i-1];
}
else if(q==mod-1)
{
p[0]=0;p[1]=1;p[2]=mod-1;
for(int i=3;i<=n;i++)p[i]=((Z)12-4*i)*Inv(i)*p[i-2];
}
else
{
f.resize(n+1);g.resize(n+1);f[0]=1;
for(int i=1;i<=n;i++)f[i]=power((Z)q,2*i-1)*Inv(power((Z)q,i)-1)*f[i-1];
g[0]=0;for(int i=1;i<=n;i++)g[i]=power((Z)q,i-1)*f[i-1];
p=g*f.inv(n);p.resize(n);
}
p[0]+=1;
Z ans=(p.pow(n,n))[n-1]*Inv(n);
cout<<ans*power((Z)q,n-1)<<endl;
return 0;
}
:::
:::info[方法三]
const int mod=998244353;
Z Inv(Z x){return power(x,mod-2);}
int n,q;
Poly p,f,g;
struct Matrix
{
Poly a[2][2];
Poly*operator[](const int&x){return a[x];}
Matrix repx(Z q)
{
Matrix z(*this);
for(int i=0;i<2;i++)for(int j=0;j<2;j++)
{
Z v=1;
for(int k=0;k<a[i][j].size();k++,v*=q)z[i][j][k]*=v;
}
return z;
}
Matrix operator*(const Matrix&rhs)
{
Matrix z;
for(int i=0;i<2;i++)for(int j=0;j<2;j++)for(int k=0;k<2;k++)
z[i][j]=z[i][j]+a[i][k]*rhs.a[k][j];
return z;
}
};
signed main()
{
cin>>n>>q;n+=2;
p.resize(n+1);
Matrix M;
M[0][0]=M[1][0]=Poly({1});M[1][1]=Poly();
M[0][1]=Poly({0,(Z)mod-1});
for(int c=1;c<=n+1;c<<=1)M=M.repx(power((Z)q,c))*M;
Poly A=-M[0][1],B=M[0][0];
p=A*B.inv(n);p.resize(n);
p[0]+=1;
Z ans=(p.pow(n,n))[n-1]*Inv(n);
cout<<ans*power((Z)q,n-1)<<endl;
return 0;
}
:::
I
:::info[题面]
给长度为
进行构造:把
:::
主播现场没过,不要嘲讽主播好喵。
先考虑对于给定的序列,如何求出最大的 anagram 长度。考虑随意构造出一些解,一些显然的情况是,任取两个下标
那么,在我们这个 naive 的构造下,最大的 anagram 长度显然是最远的两个相同元素的距离。我们大胆猜想不存在更大的 anagram 长度。下面用反证法证明:
:::info[证明]
设最远的两个相同元素的距离为
-
若
y<p ,那么考虑a_x 这个元素,由于\forall i\in[p,q], p-x\ge y-x+1 \to i-x>k ,那么[p,q] 里不存在与a_x 相等的元素,矛盾。 -
若
y\ge p ,那么由于[x,y]\cap[p,q]=[p,y] ,那么我们可以先让两个区间中[p,y] 区间内的元素相互匹配。那么转化成[x,p-1],[y+1,q] 两个区间是 anagram 的。同理,由于y+1-x>k ,也产生矛盾。
综上,anagram 长度不会超过
经过这么一化简,题目就显得可做多了。由于这显然有单调性,先无脑套个二分上去,问题变为判断是否存在一种构造使得存在每对相同元素的距离都小于等于
对于值
假设在原序列中值为
由于最终序列的位置上每个元素的值都不为
那么,这可以看作一个如下的贪心问题。
我们有
k 个长度为x 的区间,每个区间有一个活动范围,是否存在一种放置方案,能覆盖长度为m 的区间。
那么从左向右扫描,假设当前扫描到了元素
-
如果元素
i 被覆盖,跳过 -
如果元素
i 没被覆盖:取出当前所有区间中左端点调整到最右侧时最小的,取出他,并调整到恰好可以覆盖i 的时候的左端点的最右侧。不行就接着递归。
代码就不放了,嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤。
L
:::info[题面]
现在有两种类型的树 Tree1 以及 Tree2,且节点数均为
Tree1 满足根节点为
Tree2 满足根节点为
现在需要构建一个 Tree1 到 Tree2 的双射,并且满足对于任意节点
:::
:::info[方法
手摸一下小样例:
如果你再手摸一下小样例的话,你会自然而然有这么一个猜想:(从 Tree1 转到 Tree2)
-
如果是一条链,
1\to 2\to ...n ,那么要变成n\to 1,n\to 2...n\to n-1 。 -
如果是一个菊花图
1\to 2,1\to 3...1\to n ,那么要变成n\to n-1\to ...\to 2\to 1 。显然1 只能放到2 后面,因为我们要确保2 不是叶子。 -
那么假设变为
0\to 1\to 2,1\to 3...1\to n ,那么我们要么把新增加的0 放到2 后面,要么放到n 后面,这是两种看起来很有道理的方法。 -
然后,再思考一会,我们会发现放到
n 后面是挺有道理的,因为对于放在2 后面的情况,在扩展的时候,我们不确定应该把新加入的点放在那里;而对于前一种情况,我们只需要把他放到当前的根节点即可。
那么,具体化的,我们设 Tree1 中
- 那么对于非叶节点
x ,设儿子为son_1,son_2...son_k ,并且有root_{son_1}<root_{son_2}<...<root_{son_k} 那么我们这样子构造Tr_n : - 令
root_x=root_{son_k} ,并连边:root_{son_k}\to root_{son_{k-1}}..root_{son_1}\to x 。
那么正确性显然:我们构造的方案满足 Tree2 中父亲节点下标大于子节点下标的限制,同时在非叶子节点的合并的时候我们已经确保了所有
同时,我们能感觉到,这应该是一个双射:对于两个不同的树(
那么我们大胆尝试构造从 Tree2 到 Tree1 的逆构造:
- 对于当前的根节点
x ,找到当前树中下标最小的点(设为p ),那么在原来的 Tree1 上,点p 是根节点,他的儿子是x\rightsquigarrow p 路径上除了p 的所有点。那么先连边:\forall i\in x\rightsquigarrow p\And i\not=p,p\to i ,然后递归分治即可。
可以参考我的实现:
对于每个点
i 维护一个tag_i 标记,如果tag_i=1 表示在递归中已经递归到了i 为根的情况或i 的子树为根的情况。那么从
1\to n 扫描每个点,对于点i ,如果tag_i=1 ,跳过,否则如果tag_i=0 ,说明他还没有成为任何一个递归问题中子树的根,那么让i 不断往上跳跳到抵押给tag_{p}=1 的点p ,那么点i 在递归问题中应该处于以p 为根的子树内,连边路径上的点并且对路径上的点打标记即可。由于一个点一旦
tag_i=1 就不会再访问他的父亲,因此复杂度是O(n) 的
::::info[代码]
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+100;
int r,T,n;
int fa1[N],fa2[N];
vector<int>g[N],q[N];
int dfsA(int x)
{
vector<int>now;
for(auto y:g[x])now.push_back(dfsA(y));
if(now.empty())return x;
sort(now.begin(),now.end());reverse(now.begin(),now.end());
for(int i=1;i<now.size();i++)fa2[now[i]]=now[i-1];
fa2[x]=now.back();
return now[0];
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>r>>T;while(T--)
{
cin>>n;
for(int i=0;i<=n;i++)g[i].clear(),fa1[i]=fa2[i]=0;
for(int i=1;i<=n;i++)cin>>fa1[i],g[fa1[i]].push_back(i);
if(r==1)dfsA(1);
else for(int i=1;i<=n;i++)
{
//用 fa1[p]=0 表示 tag_p=1
int p=i,t;while(fa1[p]){t=fa1[p];fa1[p]=0;fa2[p]=i;p=t;}
if(i!=p)fa2[i]=fa2[p],fa2[p]=i;
}
for(int i=1;i<=n;i++)cout<<fa2[i]<<" ";cout<<endl;
}
return 0;
}
::::
:::info[方法
(可以看 SUA 官方题解。)
一种截然不同的思路,考虑从 Tree1 递增的构造 Tree2,由于 Tree1 特殊的性质,假设已经构造好了前
-
首先在 Tree2 中连边
n\to n_{n-1} 。 -
如果
p_i 原先的 Tree1 是叶子,由于新增加了n 号点,他要在 Tree2 中p_i 要从非叶子变成叶子,那么我们抽出在 Tree2 中p_i\rightsquigarrow n 的所有点(设为a_0=p_n\to a_1...\to a_{m}=n-1\to a_{m+1}=n ),一个 naive 的想法是把a_0 的所有儿子移到a_1 上,把a_1 原先的儿子移到a_2 上……容易发现,a_0 从原本的非叶子变成了叶子,而其他点由于在链上且不是链的尾端,因此儿子不管怎么动都是非叶子。 -
有小伙伴可能就会问,主播主播,我们不是只需要把
a_0 的儿子放到a_1 上就可以了吗,为什么要搞这么复杂?如果只有这个操作,你很容易发现在 Tree2 中你不知道如何描述这个映射的逆操作,那么这个想法大概不是双射的。也基于这样的想法,所以才会有儿子按顺位移动的想法。 -
如果
p_i 不是叶子,那么一种显然的想法是我们不需要额外做任何事情,但显然这样对于只有n 号点的父亲不同的两颗 Tree1 树,如果父亲都不是叶子,那么我们无法区分。 -
既然这样我们还是模仿上面瞬移。
这个操作看起来是双射的,但怎么逆回去看着没有太多思路,再仔细观察,会发现一个神奇的事情:
对于一次操作
a_0=p_n\to a_1...\to a_{m}=n-1\to a_{m+1}=n ,假设 Tree2 中前n-1 个点构成的树中a_i 的儿子构成的集合除去a_{i-1} 为son_{a_i} ,那么可以发现\forall x\in son_{a_i},x<a_i\And x<a_{i+1} ,也就是说,在操作完后,a_i 的儿子集合为a_{i-1} 和son_{a_{i-1}} ,且a_i 中编号最大的儿子为a_{i-1} 。也就是说,在 Tree2 中前
n 个点构成的树中,定义重儿子为编号最大的儿子,那么找到以n 开头的重链就是n-1 时的操作的a 序列。
那么我们可以构造出从 Tree2 映射到 Tree1 的操作:
假设当前递归到了
n 号点,找出n 号点的重链(设为a_0=p_n\to a_1\to ...\to a_{m+1}=n ),那么令p_n=a_0 ,然后把a_i 的儿子移到a_{i-1} 下面,然后把n 号点删除,递归操作。
::::info[代码]
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+100;
int r,T,n;
int p[N],q[N];
set<int>g[N];
int main()
{
cin>>r>>T;
while(T--)
{
cin>>n;
for(int i=1;i<=n;i++)cin>>p[i],g[i].clear(),q[i]=0;
if(r==1)
{
for(int i=2;i<=n;i++)
{
q[i-1]=i;g[i].insert(i-1);
int x=p[i];
vector<int>vt;while(x)vt.push_back(x),x=q[x];
for(int j=vt.size()-1;j>0;j--)
{
g[vt[j]]=g[vt[j-1]];g[vt[j]].insert(vt[j-1]);
if(j-2>=0)g[vt[j]].erase(vt[j-2]);
for(auto y:g[vt[j]])q[y]=vt[j];
}
g[vt[0]].clear();
}
}
else
{
for(int i=1;i<=n;i++)g[p[i]].insert(i);
for(int i=n;i>1;i--)
{
vector<int>vt;
int x=i;
while(x)vt.push_back(x),x=(g[x].empty()?0:*--g[x].end());
reverse(vt.begin(),vt.end()),q[i]=vt[0];
for(int j=0;j<vt.size()-1;j++)
{
g[vt[j]]=g[vt[j+1]];g[vt[j]].erase(vt[j]);
if(j-1>=0)g[vt[j]].insert(vt[j-1]);
for(auto y:g[vt[j]])p[y]=vt[j];
}
g[vt.back()].clear();
}
}
for(int i=1;i<=n;i++)cout<<q[i]<<" ";cout<<endl;
}
}
::::
:::info[方法
主要是 提交记录 #2016963 - QOJ.ac 的思路。
如果你是数学大神的话,你会发现,对于
也就是说这两者可能存在某种双射,那么我们进行如下构造:
设
P_i 表示i 的后继,即(i\to P_i) ,设p_i 表示i 在 Tree1 中的父亲。增量构造,现已经构造了前
n-1 个点对应的长度为n-1 的轮换,考虑加入第n 个点:
- 原本是:
p_i\to P_{p_i} ,插入i ,改为p_i\to i\to P_{p_i} 。
由于显然存在唯一的逆运算,并且构造如下:
设
pre_i 表示存在pre_i\to i 这条边,即i 的前驱。从
n\to 1 递归,对于当前点i ,显然pre_i 应该是i 的父亲,那么令p_{i}=pre_i ,并且把边从原来的P_{pre_i}\to i\to P_i 改为P_{pre_i}\to P_{i} ,同时从pre_{P_i}=i 改为pre_{P_i}=pre_i 。
那么可以看出来这是一个双射。
另外,我们发现这个构造里面存在一些优美的性质:
由于每次插入时都是形如
p_i\to i\to P_{p_i} 的形式,这说明如果p_i 为非叶子,那么在1\to n 的扫描过程中,每次遇到一个j 满足p_i 是j 的父亲,P_{p_i} 就会被更新为j ,而在其他情况下P_{p_i} 不变。
也就是说,如果
i 为非叶子,那么P_{i} 恰好就是i 中的所有儿子编号中最大的一个,即i<P_{i} 如果
i 为叶子,那么P_i 只会在插入i 的时候被更新一次,也就是说i>P_i
既然 Tree2 要求我们将叶子变为非叶子,将叶子变为非叶子,一个呼之欲出的想法是如果令
那么我们创立一个新置换
- 在置换
P 对应的 Tree1 中的树中i 是叶子\iff 在置换Q 对应的 Tree1 中的树中n-i+1 是非叶子 - 在置换
P 对应的 Tree1 中的树中i 是非叶子\iff 在置换Q 对应的 Tree1 中的树中n-i+1 是叶子
不过我们想要推出在 Tree2 中的树,由于上面关系中
对于 Tree1 的一颗树树中的一条边
(i\to p'_i) ,构造 Tree2 中的边n-i+1\to n-p_i+1 。由于之前
i>p'_i ,之后有n-i+1<n-p_{i}+1 ,也就是说我们的构造合法。也就是说 Tree1 可以不改变树形态的转换到 Tree2。只是每个节点的下标从i 变成了n-i+1 。那么,置换
Q 对应的 Tree1,我们把他不改变树形态的转换到 Tree2,有:
- 在置换
P 对应的 Tree1 中的树中i 是叶子\iff 在置换Q 对应的 Tree1 中的树中n-i+1 是非叶子\iff 在置换 Q 对应的 Tree1 中生成的对应 Tree2 的树中i 是非叶子- 在置换
P 对应的 Tree1 中的树中i 是非叶子\iff 在置换Q 对应的 Tree1 中的树中n-i+1 是叶子\iff 在置换 Q 对应的 Tree1 中生成的对应 Tree2 的树中i 是叶子
这样就完成了 Tree1 到 Tree2 的构造。果然很神秘。
另外,对于 Tree2 映射到 Tree1 的情况,我们先将 Tree2 不改变树形态的转化为 Tree1(设其对应的置换为
- Tree2 中
i 为叶子\iff 在置换P 对应的 Tree1 中的树中n-i+1 是非叶子\iff 在置换Q 对应的 Tree1 中的树中i 是非叶子。 - Tree2 中
i 为非叶子\iff 在置换P 对应的 Tree1 中的树中n-i+1 是叶子\iff 在置换Q 对应的 Tree1 中的树中i 是叶子。
那么,本题就被神秘的解决了。
::::info[代码]
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+100;
int r,T,n;
int p[N],P[N],q[N],pre[N];
void rev(int *a)
//可以看到,置换 P 变换到置换 Q,或者 Tree1 和 Tree2 形态不变的互相转化都是这么写的
{
reverse(a+1,a+n+1);
for(int i=1;i<=n;i++)a[i]=!a[i]?0:n-a[i]+1;//根的话强制父亲为 0
}
int main()
{
cin>>r>>T;
while(T--)
{
cin>>n;
for(int i=1;i<=n;i++)cin>>p[i],P[i]=i;
if(r==2)rev(p);
for(int i=2;i<=n;i++)swap(P[p[i]],P[i]);
rev(P);
for(int i=1;i<=n;i++)pre[P[i]]=i;
q[1]=0;for(int i=n;i>=2;i--)q[i]=pre[P[i]]=pre[i],P[pre[i]]=P[i];
if(r==1)rev(q);
for(int i=1;i<=n;i++)cout<<q[i]<<' ';cout<<endl;
}
}
::::