7.13晚考试总结(NOIP模拟14)[队长快跑·影魔·抛硬币]
樱花满地集于我心,楪舞纷飞祈愿相随
前言
终于没有令人头疼的数学了,总感觉这次考试的题目比较良心。
尤其是对于部分分的明细就非常到位(不像上一场的凭感觉给出部分分)。
这就令我这种靠部分分暴力的菜鸡选手非常得劲了。。。
下面的话也会讲解一下暴力的打法。
T1 队长快跑
解题思路
暴力
其实也不算太暴力,就是一个比较菜一点的 \(\mathcal{O(n^3)}\) 的暴力 DP 。
毕竟我们看到了那醒目的:
对于 \(60\%\) 的数据保证 \(n,m\le 200\)
转移柿子也非常的简单就是在 \(n^2\) 的线性 DP 的基础上加了一个对于之前最小值的更新。
然后再加一个离散化(好像也无所谓)。
至于 DP 数组,开二维,分别记录最后是以第几位结尾,最小的 A 是多少,进行转移就好了。
for(int i=2;i<=n;i++)
for(int j=1;j<i;j++)
for(int k=b[i]+1;k<=cnt;k++)
f[i][min(a[i],k)]=max(f[i][min(a[i],k)],f[j][k]+1);
如果直接暴力计算每一种情况并记录的话,也是有 30pts 的(毕竟我试过了\(code\))
正解
正解其实就是优化了一下 DP 方程后加了一棵线段树维护。
DP 数组与暴力的一样是二维的,只不过第一维表示的是前 i 位中的最优解而非以 i 结尾的。
转移方程分为两种情况:
-
\(A_i\le B_i\)的情况就是:\(f_{i,A_i}=\sum\limits_{j=B_i+1}^{MAX} max\{f_{i-1,j}\}\)
-
\(A_i>B_i\)的情况就是:\(f_{i,A_i}=\sum\limits_{j=A_i+1}^{MAX} max\{f_{i-1,j}\}\)
并且,对于\(j\in (B_i,A_i]\)要更新\(f_{i,j}=f_{i-1,j}+1\)
然后,显然的这个可以用线段树进行维护(区间最值查询,区间修改,单点修改就对了)。
如果 DP 方程一顿猛敲的话也是 60pts (\(code\))
code
暴力
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read()
{
int x=0,f=1;
char ch=getchar();
while(ch>'9'||ch<'0')
{
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*f;
}
const int N=1e5+10,INF=1e9,M=1e3+10;
int n,ans,cnt,a[N],b[N],lsh[N],f[M][M];
signed main()
{
n=read();
for(int i=1;i<=n;i++)
{
a[i]=read();
b[i]=read();
lsh[++cnt]=a[i];
lsh[++cnt]=b[i];
}
sort(lsh+1,lsh+cnt+1);
cnt=unique(lsh+1,lsh+cnt+1)-lsh-1;
for(int i=1;i<=n;i++)
{
a[i]=lower_bound(lsh+1,lsh+cnt+1,a[i])-lsh;
b[i]=lower_bound(lsh+1,lsh+cnt+1,b[i])-lsh;
}
f[1][a[1]]=1;
for(int i=2;i<=n;i++)
for(int j=1;j<i;j++)
for(int k=b[i]+1;k<=cnt;k++)
f[i][min(a[i],k)]=max(f[i][min(a[i],k)],f[j][k]+1);
for(int i=1;i<=n;i++)
for(int j=1;j<=cnt;j++)
ans=max(ans,f[i][j]);
printf("%lld",ans);
return 0;
}
正解
#include<bits/stdc++.h>
#define int long long
#define ls x<<1
#define rs x<<1|1
using namespace std;
inline int read()
{
int x=0,f=1;
char ch=getchar();
while(ch>'9'||ch<'0')
{
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*f;
}
const int N=2e5+10,INF=1e9;
int n,ans,cnt,a[N],b[N],lsh[N];
int tre[N<<2],laz[N<<2];
void push_up(int x)
{
tre[x]=max(tre[ls],tre[rs]);
}
void push_down(int x)
{
if(!laz[x]) return ;
tre[x]+=laz[x];
laz[ls]+=laz[x];
laz[rs]+=laz[x];
tre[ls]+=laz[x];
tre[rs]+=laz[x];
laz[x]=0;
}
int query(int x,int l,int r,int L,int R)
{
if(L<=l&&r<=R)
return tre[x];
push_down(x);
int mid=(l+r)>>1,maxn=0;
if(L<=mid) maxn=query(ls,l,mid,L,R);
if(R>mid) maxn=max(maxn,query(rs,mid+1,r,L,R));
push_up(x);
return maxn;
}
void update(int x,int l,int r,int pos,int num)
{
if(l==r)
{
tre[x]=max(tre[x],num);
return ;
}
push_down(x);
int mid=(l+r)>>1;
if(pos<=mid) update(ls,l,mid,pos,num);
else update(rs,mid+1,r,pos,num);
push_up(x);
}
void change(int x,int l,int r,int L,int R,int num)
{
if(L<=l&&r<=R)
{
tre[x]+=num;
laz[x]+=num;
return ;
}
push_down(x);
int mid=(l+r)>>1,total=0;
if(L<=mid) change(ls,l,mid,L,R,num);
if(R>mid) change(rs,mid+1,r,L,R,num);
push_up(x);
}
signed main()
{
n=read();
for(int i=1;i<=n;i++)
{
a[i]=read();
b[i]=read();
lsh[++cnt]=a[i];
lsh[++cnt]=b[i];
}
sort(lsh+1,lsh+cnt+1);
cnt=unique(lsh+1,lsh+cnt+1)-lsh-1;
for(int i=1;i<=n;i++)
{
a[i]=lower_bound(lsh+1,lsh+cnt+1,a[i])-lsh;
b[i]=lower_bound(lsh+1,lsh+cnt+1,b[i])-lsh;
}
for(int i=1;i<=n;i++)
if(a[i]<=b[i])
{
int maxn=query(1,1,cnt,b[i]+1,cnt);
update(1,1,cnt,a[i],maxn+1);
}
else
{
change(1,1,cnt,b[i]+1,a[i],1);
int maxn=query(1,1,cnt,a[i]+1,cnt);
update(1,1,cnt,a[i],maxn+1);
}
printf("%lld",query(1,1,cnt,1,cnt));
return 0;
}
T2 影魔
解题思路
暴力
其实这个题我一开始是看错题了的,题面中的 d 是子树的根节点到子树中的节点的距离而非子树中各节点之间的距离。
首先,对于特殊性质:
对于 \(20\%\) 的数据保证每次询问满足 \(d=10^9\)
这不就不用考虑到根节点的距离了吗??
于是我们勤勤恳恳地在求出一边 DFS 序之后直接莫队,于是我们就有了 20pts 。
对于 \(30\%\) 的较小的 n,m 的数据,直接暴力就可以过。
然后对于 \(10\%\) 的随机数据,诶随机的数一般都比较大,我们对于 d 大于子树大小的像 \(d=10^9\) 的一样记录下来,然后莫队,其他的直接暴力就好了。
唯一的缺陷就是码量有一点大。。也就 200 来行吧(算上快读和树剖,其实树剖没啥用)。
正解
正解是主席树+set(\(code\),来自 pyt),但是正经人谁打主席树呀,我直接一手线段树合并。。
为了方便,以下称灵魂种类为颜色。
开两棵权值线段树分别是:建立在颜色上的深度,建立在深度上的对于颜色的种类数。
对于这个树从下向上进行合并更新。
对于第一棵线段树的合并过程中可以一直搜到 \(l=r\) 的只可能是同种颜色,因此我们只需要在此基础上对于第二棵线段树上进行修改就好了。
注意在合并的时候不可以直接在原节点进行修改,而是应该新开一棵线段树进行记录,因为我们要留下以前的数据。
然后对于后面的求和直接在第二棵线段树上进行就好了。
code
暴力
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read()
{
int x=0,f=1;
char ch=getchar();
while(ch>'9'||ch<'0')
{
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*f;
}
const int N=1e5+10,M=2e4,INF=1e9;
int n,m,tim,id[N],dep[N],son[N],siz[N],l[N],r[N],fa[N],topp[N],clo[N];
int tot,head[N],nxt[N],ver[N];
int cnt[N];
bool flag;
struct Ques
{
int rt,d,id;
}q[N];
void add_edge(int x,int y)
{
ver[++tot]=y;
nxt[tot]=head[x];
head[x]=tot;
}
void dfs1(int x)
{
siz[x]=1;
for(int i=head[x];i;i=nxt[i])
{
int to=ver[i];
dep[to]=dep[x]+1;
dfs1(to);
siz[x]+=siz[to];
if(siz[to]>siz[son[x]])
son[x]=to;
}
}
void dfs2(int x,int tp)
{
topp[x]=tp;
l[x]=++tim;
id[tim]=x;
if(son[x]) dfs2(son[x],tp);
for(int i=head[x];i;i=nxt[i])
if(!l[ver[i]])
dfs2(ver[i],ver[i]);
r[x]=tim;
}
int LCA(int x,int y)
{
if(!x||!y) return 0;
while(topp[x]^topp[y])
{
if(dep[topp[x]]<dep[topp[y]])
swap(x,y);
x=fa[topp[x]];
}
if(dep[x]>dep[y])
swap(x,y);
return x;
}
int li=1,ri,res,pos[N],ans[N];
bool vis[N],b[N];
vector<int> v;
bool comp(Ques x,Ques y)
{
if(pos[l[x.rt]]==pos[l[y.rt]])
return r[x.rt]<r[y.rt];
return l[x.rt]<l[y.rt];
}
void delate(int x)
{
cnt[clo[id[x]]]--;
if(!cnt[clo[id[x]]]) res--;
}
void add(int x)
{
cnt[clo[id[x]]]++;
if(cnt[clo[id[x]]]==1) res++;
}
void solve1()
{
int len=pow(n,0.6666666666);
for(int i=1;i<=n;i++)
pos[i]=(i-1)/len+1;
sort(q+1,q+m+1,comp);
for(int i=1;i<=m;i++)
{
while(l[q[i].rt]>li)
delate(li),li++;
while(l[q[i].rt]<li)
add(li-1),li--;
while(r[q[i].rt]>ri)
add(ri+1),ri++;
while(r[q[i].rt]<ri)
delate(ri),ri--;
if(l[q[i].rt]==r[q[i].rt])
{
ans[q[i].id]=1;
continue;
}
ans[q[i].id]=res;
}
}
int dist(int x,int y)
{
return dep[x]+dep[y]-2*dep[LCA(x,y)];
}
void change(int fro,int to)
{
if(fro==to)
return ;
while(fa[fro]!=fa[to]&&fro)
{
if(!b[fro]) v.push_back(fro);
b[fro]=true;
fro=fa[fro];
}
if(!b[fro]) v.push_back(fro);
b[fro]=true;
}
int work(int rt,int d)
{
if(!d) return 1;
int ls=l[rt],rs=r[rt],sum=0;
for(int i=ls;i<=ls;i++)
{
int su=0;
vector<int>().swap(v);
for(int j=ls;j<=rs;j++)
{
if(b[id[j]]||j==i||dist(id[i],id[j])>d) continue;
int lca=LCA(id[i],id[j]);
change(id[i],lca);
change(id[j],lca);
}
for(int k=0;k<v.size();k++)
if(!vis[clo[v[k]]]&&v[k])
{
su++;
vis[clo[v[k]]]=true;
}
for(int k=0;k<v.size();k++)
vis[clo[v[k]]]=b[v[k]]=false;
sum=max(sum,su);
}
return sum;
}
void solve2()
{
Ques s[m+5];
for(int i=1;i<=m;i++)
s[i]=q[i];
int temp=m;
m=0;
for(int i=1;i<=temp;i++)
if(siz[s[i].rt]<=s[i].d)
q[++m]=s[i];
if(m) solve1();
m=temp;
for(int i=1;i<=m;i++)
if(!ans[i])
ans[i]=work(s[i].rt,s[i].d);
}
signed main()
{
n=read();
m=read();
for(int i=1;i<=n;i++)
clo[i]=read();
for(int i=2;i<=n;i++)
{
fa[i]=read();
add_edge(fa[i],i);
}
dfs1(1);
dfs2(1,1);
for(int i=1;i<=m;i++)
{
q[i].rt=read();
q[i].d=read();
q[i].id=i;
if(q[i].d!=INF) flag=true;
}
if(!flag) solve1();
solve2();
for(int i=1;i<=m;i++)
printf("%lld\n",ans[i]);
return 0;
}
正解
#include<bits/stdc++.h>
#define int long long
#define ls tre[x].l
#define rs tre[x].r
using namespace std;
inline int read()
{
int x=0,f=1;
char ch=getchar();
while(ch>'9'||ch<'0')
{
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*f;
}
const int N=1e5+10,INF=1e9;
int n,m,dep[N],fa[N],clo[N];
int tot,head[N],nxt[N],ver[N];
int all,root1[N],root2[N];
int cnt,lsh[N];
struct Segment_Tree
{
int l,r,val;
}tre[N*120];
void add_edge(int x,int y)
{
ver[++tot]=y;
nxt[tot]=head[x];
head[x]=tot;
}
void dfs1(int x)
{
dep[x]=dep[fa[x]]+1;
for(int i=head[x];i;i=nxt[i])
dfs1(ver[i]);
}
void build(int &x,int l,int r,int pos,int val)//记录颜色上的深度
{
if(!x) x=++all;
if(l==r)
{
tre[x].val=val;
return ;
}
int mid=(l+r)>>1;
if(pos<=mid) build(ls,l,mid,pos,val);
else build(rs,mid+1,r,pos,val);
}
void update(int &x,int l,int r,int pos,int val)//记录深度上的数量
{
++all;
tre[all]=tre[x];
x=all;
tre[x].val+=val;
if(l==r) return ;
int mid=(l+r)>>1;
if(pos<=mid) update(ls,l,mid,pos,val);
else update(rs,mid+1,r,pos,val);
}
int merge1(int x,int y,int l,int r,int pos)
{
if(!x||!y) return x+y;
int rt=++all;
if(l==r)
{//合并的同时对于为x的进行更新
tre[rt].val=min(tre[x].val,tre[y].val);//因为是建立在颜色上的深度,因此取较小的深度记录
update(root2[pos],1,n,max(tre[x].val,tre[y].val),-1);//对于自身的取子树中的深度进行更改,也就是减去子树的部分
return rt;
}
int mid=(l+r)>>1;
tre[rt].l=merge1(tre[x].l,tre[y].l,l,mid,pos);
tre[rt].r=merge1(tre[x].r,tre[y].r,mid+1,r,pos);
return rt;
}
int merge2(int x,int y,int l,int r)//直接合并子树
{
if(!x||!y) return x+y;
int rt=++all;
tre[rt].val=tre[x].val+tre[y].val;
int mid=(l+r)>>1;
tre[rt].l=merge2(tre[x].l,tre[y].l,l,mid);
tre[rt].r=merge2(tre[x].r,tre[y].r,mid+1,r);
return rt;
}
void dfs2(int x)
{
build(root1[x],1,cnt,clo[x],dep[x]);//建立在颜色上的树记录深度
update(root2[x],1,n,dep[x],1);//建立在深度上记录大小
for(int i=head[x];i;i=nxt[i])
{
int to=ver[i];
dfs2(to);//向下搜
root1[x]=merge1(root1[x],root1[to],1,cnt,x);//将子树的合并上来并且顺便将自身进行修改
root2[x]=merge2(root2[x],root2[to],1,n);//在将子树深度上的大小合并记录上来
}
}
int query(int x,int l,int r,int L,int R)//求值
{
if(L<=l&&r<=R)
return tre[x].val;
int mid=(l+r)>>1,sum=0;
if(L<=mid) sum+=query(ls,l,mid,L,R);
if(R>mid) sum+=query(rs,mid+1,r,L,R);
return sum;
}
signed main()
{
n=read();
m=read();
for(int i=1;i<=n;i++)
{
clo[i]=read();
lsh[++cnt]=clo[i];
}
sort(lsh+1,lsh+cnt+1);
cnt=unique(lsh+1,lsh+cnt+1)-lsh-1;
for(int i=1;i<=n;i++)
clo[i]=lower_bound(lsh+1,lsh+cnt+1,clo[i])-lsh;
for(int i=2;i<=n;i++)
{
fa[i]=read();
add_edge(fa[i],i);
}
dfs1(1);
dfs2(1);
for(int i=1,rt,d;i<=m;i++)
{
rt=read();
d=read();
printf("%lld\n",query(root2[rt],1,n,dep[rt],dep[rt]+d));
}
return 0;
}
T3 抛硬币
解题思路
暴力
这个题应该是三个题里边最简单的了,但是我难得一次 123 开题,竟然搞这一套。
幸亏这个题的暴力分也比较可观,对于每一个位置的选择情况只能有两种。
因此对于 \(|S|\le15\) 的可以直接 \(\mathcal{O}(2^n)\) 搞掉。
然后还有特殊性质:
对于 \(20\%\) 的数据保证 \(S\) 只由一种字符组成。
这种情况直接输出 1 就好了。
正解
3000 的数据 \(\mathcal{O}(n^2)\) 的 DP 就足够了。
接下来就是裸的线性 DP 了。
设 \(f_{i,j}\) 表示处理完串的前 i 个位置,长度为 j 的本质不同的子序列个数。
转移考虑添加一个尾部字符,直接想法是 \(f_{i,j}=f_{i-1,j}+f_{i-1,j-1}\)。
然而这样我们会算重,考虑把算重的部分减掉。
算重的部分一定是以为 \(s_i\) 结尾的串。所以设上一次出现的位置为 \(pre\) 。
那么以 \(s_i\) 结尾的串与算重的串一一对应。
因此可以的出转移方程式:\(f_{i,j}=f_{i-1,j}+f_{i-1,j-1}-f_{pre-1,j-1}\)
注意这里是 \(pre-1\) ,因为重复的情况就是 这个位置补上了前面同一种字符的位置。
code
暴力
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read()
{
int x=0,f=1;
char ch=getchar();
while(ch>'9'||ch<'0')
{
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*f;
}
const int N=3e3+10,mod=998244353;
int n,m,ans;
bool flag;
char ch[N];
string s,c;
map<string,bool> vis;
void dfs(int pos,int len)
{
if(len==m+1)
{
string ().swap(c);
for(int i=1;i<=m;i++)
c.push_back(ch[i]);
if(!vis[c]) ans=(ans+1)%mod;
vis[c]=true;
return ;
}
if(pos>n) return ;
dfs(pos+1,len);
ch[len+1]=s[pos];
dfs(pos+1,len+1);
}
signed main()
{
cin>>s;
n=s.size();
m=read();
for(int i=1;i<n;i++)
if(s[i]!=s[i-1])
flag=true;
if(!flag||m>n)
{
cout<<1;
return 0;
}
dfs(0,0);
printf("%lld",ans%mod);
return 0;
}
正解
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read()
{
int x=0,f=1;
char ch=getchar();
while(ch>'9'||ch<'0')
{
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*f;
}
const int N=3e3+10,mod=998244353;
int n,m,f[N][N],pre[30];
string s;
signed main()
{
cin>>s;
n=s.size();
s=" "+s;
m=read();
f[0][0]=1;
for(int i=1;i<=n;i++)
{
f[i][0]=1;
for(int j=1;j<=min(i,m);j++)
{
f[i][j]=(f[i][j]+f[i-1][j]+f[i-1][j-1])%mod;
if(pre[s[i]-'a']) f[i][j]=(f[i][j]-f[pre[s[i]-'a']-1][j-1]+mod)%mod;
}
pre[s[i]-'a']=i;
}
printf("%lld",f[n][m]);
return 0;
}