HEOI2016解题报告

在2016年,佳媛姐姐刚刚学习了树,非常开心。现在他想解决这样一个问题:给定一颗有根树(根为1),有以下
两种操作:1. 标记操作:对某个结点打上标记(在最开始,只有结点1有标记,其他结点均无标记,而且对于某个
结点,可以打多次标记。)2. 询问操作:询问某个结点最近的一个打了标记的祖先(这个结点本身也算自己的祖
先)你能帮帮他吗?
题解
这题如果直接做的话,就是一个裸的树链剖分。
但也有一个更加巧妙的做法,如果把操作序列倒过来看,打标记就变成了删标记。
如果用一个数组来维护答案的话,这个操作相当于把指向这个点的位置的答案改成这个点的答案,可以直接用并查集来维护这一过程。
#include<iostream>
#include<cstdio>
#define N 100009
using namespace std;
int f[N],head[N],tot,n,q,a[N],fa[N],ans[N],top,tag[N];
char c[N];
struct dwd {
int n,to;
} e[N<<];
inline void add(int u,int v) {
e[++tot].n=head[u];
e[tot].to=v;
head[u]=tot;
}
int find(int x) {
return f[x]=f[x]==x?x:find(f[x]);
}
inline char getch() {
char c=getchar();
while(!isalpha(c))c=getchar();
return c;
}
int rd() {
int x=;
char c=getchar();
while(!isdigit(c))c=getchar();
while(isdigit(c)) {
x=(x<<)+(x<<)+(c^);
c=getchar();
}
return x;
}
void dfs(int u,int faa) {
for(int i=head[u]; i; i=e[i].n) {
int v=e[i].to;
if(v==faa)continue;
fa[v]=u;
if(!tag[v])f[v]=u;
else f[v]=v;
dfs(v,u);
}
}
int main() {
n=rd();
q=rd();
int u,v;
for(int i=; i<n; ++i)u=rd(),v=rd(),add(u,v),add(v,u);
tag[]=;
f[]=;
for(int i=; i<=q; ++i) {
c[i]=getch();
a[i]=rd();
if(c[i]=='C')tag[a[i]]++;
}
dfs(,);
for(int i=q; i>=; --i) {
if(c[i]=='C') {
tag[a[i]]--;
if(!tag[a[i]])f[a[i]]=fa[a[i]];
} else ans[++top]=find(a[i]);
}
for(int i=top; i>=; --i)printf("%d\n",ans[i]);
return ;
}

排序

在2016年,佳媛姐姐喜欢上了数字序列。因而他经常研究关于序列的一些奇奇怪怪的问题,现在他在研究一个难题
,需要你来帮助他。这个难题是这样子的:给出一个1到n的全排列,现在对这个全排列序列进行m次局部排序,排
序分为两种:1:(0,l,r)表示将区间[l,r]的数字升序排序2:(1,l,r)表示将区间[l,r]的数字降序排序最后询问第q
位置上的数字。
题解
答案具有单调性,可以直接二分答案。
然后把大于这个数的标为1,小的标成0,然后那些操作直接一通做下来就好了。
只有01两种数,线段树直接统计就可以。
#include<iostream>
#include<cstdio>
#define N 30002
using namespace std;
int la[N<<],tr[N<<],a[N],ji,tag,n,m,l[N],r[N],ta[N],ans,q;
bool b[N];
inline void pushdown(int cnt,int l1,int l2){
int sum=tr[cnt];
if(la[cnt]==){
if(l1>=sum){
tr[cnt<<]=sum;sum=;
}
else{
tr[cnt<<]=l1;sum-=l1;
}
tr[cnt<<|]=sum;
}
else{
if(l2>=sum){
tr[cnt<<|]=sum;sum=;
}
else{
tr[cnt<<|]=l2;sum-=l2;
}
tr[cnt<<]=sum;
}
la[cnt<<]=la[cnt<<|]=la[cnt];la[cnt]=;
}
int dfs(int cnt,int l,int r){
if(l==r)return tr[cnt];
int mid=(l+r)>>;
if(la[cnt])pushdown(cnt,mid-l+,r-mid);
if(mid>=q)dfs(cnt<<,l,mid);
else dfs(cnt<<|,mid+,r);
}
void build(int cnt,int l,int r){
if(l==r){
tr[cnt]=b[l];
return;
}
int mid=(l+r)>>;
build(cnt<<,l,mid);build(cnt<<|,mid+,r);
tr[cnt]=tr[cnt<<]+tr[cnt<<|];//care
}
void gett(int cnt,int l,int r,int L,int R){
if(l>=L&&r<=R){
ji+=tr[cnt];
return;
}
int mid=(l+r)>>;
if(la[cnt])pushdown(cnt,mid-l+,r-mid);
if(mid>=L)gett(cnt<<,l,mid,L,R);
if(mid<R)gett(cnt<<|,mid+,r,L,R);
}
void returnn(int cnt,int l,int r,int L,int R){
if(l>=L&&r<=R){
la[cnt]=tag;
if(ji>=r-l+){
tr[cnt]=r-l+;
ji-=tr[cnt];
}
else{
tr[cnt]=ji;ji=;
}
return;
}
int mid=(l+r)>>;
if(tag==){
if(mid>=L)returnn(cnt<<,l,mid,L,R);
if(mid<R)returnn(cnt<<|,mid+,r,L,R);
}
else{
if(mid<R)returnn(cnt<<|,mid+,r,L,R);
if(mid>=L)returnn(cnt<<,l,mid,L,R);
}
tr[cnt]=tr[cnt<<]+tr[cnt<<|];
}
bool ch(int pos){
for(int i=;i<=n;++i)b[i]=a[i]>=pos?:;
build(,,n);
for(int i=;i<=m;++i){
ji=;
tag=ta[i];
gett(,,n,l[i],r[i]);
returnn(,,n,l[i],r[i]);
}
if(dfs(,,n))return ;
else return ;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=;i<=n;++i)
scanf("%d",&a[i]);
for(int i=;i<=m;++i){scanf("%d%d%d",&ta[i],&l[i],&r[i]);if(!ta[i])ta[i]=;}
scanf("%d",&q);
int l=,r=n;
while(l<=r){
int mid=(l+r)>>;
if(ch(mid)){
ans=mid;
l=mid+;
}
else r=mid-;
}
cout<<ans;
return ;
}

序列

佳媛姐姐过生日的时候,她的小伙伴从某宝上买了一个有趣的玩具送给他。玩具上有一个数列,数列中某些项的值

可能会变化,但同一个时刻最多只有一个值发生变化。现在佳媛姐姐已经研究出了所有变化的可能性,她想请教你
,能否选出一个子序列,使得在任意一种变化中,这个子序列都是不降的?请你告诉她这个子序列的最长长度即可
。注意:每种变化最多只有一个值发生变化。在样例输入1中,所有的变化是:
1 2 3
2 2 3
1 3 3
1 1 31 2 4
选择子序列为原序列,即在任意一种变化中均为不降子序列在样例输入2中,所有的变化是:3 3 33 2 3选择子序列
为第一个元素和第三个元素,或者第二个元素和第三个元素,均可满足要求
游戏
在2016年,佳缘姐姐喜欢上了一款游戏,叫做泡泡堂。简单的说,这个游戏就是在一张地图上放上若干个炸弹,看
是否能炸到对手,或者躲开对手的炸弹。在玩游戏的过程中,小H想到了这样一个问题:当给定一张地图,在这张
地图上最多能放上多少个炸弹能使得任意两个炸弹之间不会互相炸到。炸弹能炸到的范围是该炸弹所在的一行和一
列,炸弹的威力可以穿透软石头,但是不能穿透硬石头。给定一张n*m的网格地图:其中*代表空地,炸弹的威力可
以穿透,可以在空地上放置一枚炸弹。x代表软石头,炸弹的威力可以穿透,不能在此放置炸弹。#代表硬石头,炸
弹的威力是不能穿透的,不能在此放置炸弹。例如:给出1*4的网格地图*xx*,这个地图上最多只能放置一个炸弹
。给出另一个1*4的网格地图*x#*,这个地图最多能放置两个炸弹。现在小H任意给出一张n*m的网格地图,问你最
多能放置多少炸弹
题解
考虑没有墙的情况,把每行每列看做点,如果哪个位置有炸弹,就在行和列之间连边,跑二分图匹配。
如果有墙,就在墙的右边和下面为这一行和这一列新开点就可以了。
#include<iostream>
#include<cstdio>
using namespace std;
int n,m,a[][],tot2,tot1,tot,ans,pim[],vis[],head[];
char s[];
struct dv
{
int a,b;
}b[][];
struct fd
{
int next,to;
}an[];
void add(int u,int v)
{
an[++tot].next=head[u];
an[tot].to=v;
head[u]=tot;
}
bool dfs(int pos,int tim)
{
for(int i=head[pos];i;i=an[i].next)
if(vis[an[i].to]!=tim)
{
int v=an[i].to;
vis[v]=tim;
if(!pim[v]||dfs(pim[v],tim))
{
pim[v]=pos;
return true;
}
}
return false;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=;i<=n;++i)
{
scanf("%s",s+);
for(int j=;j<=m;++j)
{
if(s[j]=='*')a[i][j]=;
else if(s[j]=='x')a[i][j]=;
else a[i][j]=;
if(a[i][j]!=)
{
if(a[i][j-]==||j==)b[i][j].a=++tot1;
else b[i][j].a=b[i][j-].a;
if(a[i-][j]==||i==)b[i][j].b=++tot2;
else b[i][j].b=b[i-][j].b;
if(a[i][j]==)add(b[i][j].a,b[i][j].b);
}
}
}
for(int i=;i<=tot1;++i)
if(dfs(i,i))ans++;
cout<<ans<<endl;
return ;
}

求和

在2016年,佳媛姐姐刚刚学习了第二类斯特林数,非常开心。

现在他想计算这样一个函数的值:
HEOI2016解题报告
S(i, j)表示第二类斯特林数,递推公式为:
S(i, j) = j ∗ S(i − 1, j) + S(i − 1, j − 1), 1 <= j <= i − 1。
边界条件为:S(i, i) = 1(0 <= i), S(i, 0) = 0(1 <= i)
你能帮帮他吗?
题解
感觉这是这年省选最难的题了。
不过也是比较套路
第二类斯特林数有一个通项
S(n,m)=(1/m!)*∑(-1)i*C(m,i)*(m-i)n
然后把前面那个阶乘放到后面去。
S(n,m)=∑((-1)i/i!)*((m-i)n/(m-i)!)
再把它放回原来的式子里。
∑2jj!∑((-1)k/k!)*(∑(j-k)i/(j-k)!)
然后我们可以把后面两个东西看做卷积,做一遍NTT就可以了(得先把等比数列拆开)。
#include<iostream>
#include<cstdio>
#define N 300002
using namespace std;
typedef long long ll;
const int mod=;
const int Gi=;
const int G=;
ll a[N],b[N],rev[N],n,jie[N],ni[N],ans,l,L;
inline ll rd(){
ll x=;char c=getchar();bool f=;
while(!isdigit(c)){if(c=='-')f=;c=getchar();}
while(isdigit(c)){x=(x<<)+(x<<)+(c^);c=getchar();}
return f?-x:x;
}
ll power(ll x,ll y){
ll ans=;
while(y){
if(y&)ans=(ans*x)%mod;
y>>=;x=(x*x)%mod;
}
return ans;
}
inline int ny(ll x){return power(x,mod-);}
inline void NTT(ll *a,int tag){
for(int i=;i<l;++i)if(i<rev[i])swap(a[i],a[rev[i]]);
for(int i=;i<l;i<<=){
ll wn=power(tag==?G:Gi,(mod-)/(i<<));
for(int j=;j<l;j+=(i<<)){
ll w=;
for(int k=;k<i;++k,(w*=wn)%=mod){
ll x=a[j+k],y=w*a[j+k+i]%mod;
a[j+k]=(x+y)%mod;a[j+k+i]=(x-y+mod)%mod;
}
}
}
}
int main(){
n=rd();jie[]=;
for(int i=;i<=n;++i)jie[i]=jie[i-]*i%mod;ni[n]=power(jie[n],mod-);
for(int i=n-;i>=;--i)ni[i]=ni[i+]*(i+)%mod;
for(int i=;i<=n;++i)a[i]=(power(-,i)*ni[i]+mod)%mod;
b[]=;b[]=n+;for(int i=;i<=n;++i)b[i]=(power(i,n+)-)*ny((i-)*jie[i]%mod)%mod;
l=,L=;
while(l<(n<<))l<<=,L++;
for(int i=;i<l;++i)rev[i]=(rev[i>>]>>)|((i&)<<(L-));
NTT(a,);NTT(b,);
for(int i=;i<l;++i)a[i]=a[i]*b[i]%mod;
NTT(a,-);int nn=ny(l);
for(int i=;i<l;++i)a[i]=a[i]*nn%mod;
for(int i=;i<=n;++i)ans=(ans+power(,i)*jie[i]%mod*a[i]%mod)%mod;
cout<<ans;
return ;
}

字符串

佳媛姐姐过生日的时候,她的小伙伴从某东上买了一个生日礼物。生日礼物放在一个神奇的箱子中。箱子外边写了
一个长为n的字符串s,和m个问题。佳媛姐姐必须正确回答这m个问题,才能打开箱子拿到礼物,升职加薪,出任CE
O,嫁给高富帅,走上人生巅峰。每个问题均有a,b,c,d四个参数,问你子串s[a..b]的所有子串和s[c..d]的最长公
共前缀的长度的最大值是多少?佳媛姐姐并不擅长做这样的问题,所以她向你求助,你该如何帮助她呢?
题解
根据国际惯例,先二分长度。
然后找出c在后缀数组中的位置,既然已经二分了答案,那么可以左右分别二分,找出后缀数组上的一段区间满足我们二分的答案。
然后这段区间如果包含a~b-mid+1,相当于我们已经找到了答案。
现在问题就是判断区间里是否有某数,用主席树解决。
还有就是这题卡常,要预处理ST表的log。
// luogu-judger-enable-o2
// luogu-judger-enable-o2
%:pragma GCC optimize()
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#define N 100002
#define inf 2e9
#define Re register
using namespace std;
int n,m,tong[N],sa[N],T[N],rank[N],y[N],height[N],p[N][],tr[N*],R[N*],L[N*],tot,a,b,c,d,logg[],lg2[N];
char s[N];
inline int minn(int x,int y){return x<y?x:y;}
inline int maxx(int x,int y){return x<y?y:x;}
inline void write(int a){
if(a>=)write(a/);
putchar(''+a%);
}
inline void writeln(int a){
if(a<){
a=-a; putchar('-');
}
write(a); puts("");
}
inline int rd(){
int x=;char c=getchar();
while(!isdigit(c)){c=getchar();}
while(isdigit(c)){x=x*+(c^);c=getchar();}
return x;
}
inline void qsort(){
for(Re int i=;i<=m;++i)tong[i]=;
for(Re int i=;i<=n;++i)tong[rank[i]]++;
for(Re int i=;i<=m;++i)tong[i]+=tong[i-];
for(Re int i=n;i>=;--i)sa[tong[rank[y[i]]]--]=y[i];
}
void SA(){
m=;
for(Re int i=;i<=n;++i)rank[i]=s[i],y[i]=i;
qsort();
for(Re int w=,p;p<n;m=p,w<<=){
p=;
for(Re int i=n-w+;i<=n;++i)y[++p]=i;
for(Re int i=;i<=n;++i)if(sa[i]>w)y[++p]=sa[i]-w;
qsort();swap(y,rank);rank[sa[]]=p=;
for(Re int i=;i<=n;++i)rank[sa[i]]=((y[sa[i-]]==y[sa[i]])&&(y[sa[i-]+w]==y[sa[i]+w]))?p:++p;
}
height[]=;
for(Re int i=,j;i<=n;++i){
if(rank[i]==)continue;
j=maxx(,height[rank[i-]]-);
while(s[i+j]==s[sa[rank[i]-]+j])j++;
height[rank[i]]=j;
p[rank[i]][]=j;
}
for(Re int i=;logg[i]<=n;++i)
for(Re int j=;j+logg[i]-<=n;++j)p[j][i]=minn(p[j][i-],p[j+logg[i-]][i-]);
}
inline int RMQ(int l,int r){
if(l>r)return inf;
int lo=lg2[r-l+];
return minn(p[l][lo],p[r-logg[lo]+][lo]);
}
bool query(int now,int la,int l,int r,int LL,int RR){
if(l>=LL&&r<=RR)return (tr[now]-tr[la])!=;
Re int mid=(l+r)>>;bool ans=;
if(mid>=LL)ans|=query(L[now],L[la],l,mid,LL,RR);if(ans)return ;
if(mid<RR)ans|=query(R[now],R[la],mid+,r,LL,RR);
return ans;
}
void upd(int &cnt,int la,int l,int r,int x){
cnt=++tot;L[cnt]=L[la];R[cnt]=R[la];tr[cnt]=tr[la]+;
if(l==r)return;Re int mid=(l+r)>>;
if(mid>=x)upd(L[cnt],L[la],l,mid,x);
else upd(R[cnt],R[la],mid+,r,x);
}
void build(int &cnt,int l,int r){
cnt=++tot;if(l==r)return;
int mid=(l+r)>>;build(L[cnt],l,mid);build(R[cnt],mid+,r);
}
bool check(int x,int len){
Re int l=,r=x,zuo=x,you=x;
while(l<=r){
Re int mid=(l+r)>>;
if(RMQ(mid+,x)>=len){
zuo=mid;r=mid-;
}else l=mid+;
}
l=x,r=n;
while(l<=r){
Re int mid=(l+r)>>;
if(RMQ(x+,mid)>=len){
you=mid;l=mid+;
}else r=mid-;
}
if(query(T[you],T[zuo-],,n,a,b-len+))return ;
else return ;
}
inline void gett(){
char c=getchar();while(!isalpha(c))c=getchar();n=;
while(isalpha(c))s[++n]=c,c=getchar();
}
int main(){
n=rd();m=rd();gett();
for(Re int i=;i<=;++i)logg[i]=(<<i);
for(Re int i=;i<=n;++i)lg2[i]=lg2[i>>]+;
SA();
build(T[],,n);
for(Re int i=;i<=n;++i)upd(T[i],T[i-],,n,sa[i]);///care!!!!!!!!!!!!!!
while(m--){
a=rd();b=rd();c=rd();d=rd();
Re int l=c,r=d,ans=c-;r=minn(r,c+b-a);
while(l<=r){
Re int mid=(l+r)>>;
if(check(rank[c],mid-c+)){ans=mid;l=mid+;}
else r=mid-;
}
writeln(ans-c+);
}
return ;
}
这一年的题好水
上一篇:C# 拷贝数组的几种方式


下一篇:Spring+Quartz 实现定时任务的配置方法