A. Stickers and Toys
http://codeforces.com/contest/1187/problem/A
签到题,给s个便签,t个玩具,塞进n个蛋里(暗示奇趣蛋?),保证没有蛋是空的,问至少取几个蛋可以保证得到至少一个便签和一个玩具。
因为没有蛋是空的,所以可以算出有s+t-n个蛋里两种都有,n-s个蛋里只有玩具,n-t个蛋里只有便签。所以最坏情况是取了n-s个玩具,或者n-t个便签,再之后随便取什么都能满足条件,所以直接输出 a n s = m a x ( n − s , n − t ) + 1 ans=max(n-s,n-t)+1 ans=max(n−s,n−t)+1。
复杂度O(1)
B. Letters Shop
http://codeforces.com/contest/1187/problem/B
给一个字符串s,接下来m组询问,问字符串每组给定一个字符串t,如果取s的前缀来重组成t(可以过量),问至少要取几位前缀。数据保证有解。
这个问题满足单调性,且全取比可行,取0个必不可行,以0和s的长度n为左右指针二分,判断mid=(l+r)>>1位置处s的前缀中各个字母的数字是否都大于t各个字母的个数,先预处理前缀和数组,就可以在O(m*26log(n))时间内完成。
但是其实还有更快的做法,直接保存s中每个字母各个字母第i次出现的位置 a i , b i , . . . . . . , z i a_i,b_i,......,z_i ai,bi,......,zi假定t中各个字母个数为 h a , h b , . . . . . . , h z h_a,h_b,......,h_z ha,hb,......,hz ,则结果 a n s = m a x ( a h a , b h b , . . . . . . , c h c ) ans=max(a_{ha},b_{hb},......,c_{hc}) ans=max(aha,bhb,......,chc) ,复杂度O(n+m)。
以下给出二分代码,因为做题的时候我想的就是二分。
#include<bits/stdc++.h>
using namespace std;
int n,m;
string s;
int cnt[26][200005];
int c[26];
bool check(int x){
for(int i=0;i<26;++i){
if(cnt[i][x]<c[i])return 0;
}
return 1;
}
int main(){
cin>>n>>s;
for(int i=1;i<=n;++i){
for(int j=0;j<26;++j)cnt[j][i]=cnt[j][i-1];
++cnt[s[i-1]-'a'][i];
}
cin>>m;
while(m--){
cin>>s;
memset(c,0,sizeof(c));
for(auto x:s)++c[x-'a'];
int l=0,r=n;
while(l<r-1){
int m=(l+r)>>1;
if(check(m))r=m;
else l=m;
}
cout<<r<<endl;
}
}
C. Vasya And Array
http://codeforces.com/contest/1187/problem/C
构造题。给你一些区间,其中一些区间要求单调不减,另一些要求不满足单调不减,也就是区间里存在
i
<
j
i<j
i<j满足
a
i
>
a
j
a_i>a_j
ai>aj。
显然上述两个是事件是对立的,对于任意一段区间,这两个事件是其可能状态的一个分割。且单调不减的结论要比他的对立事件强很多,因为他要满足任意性,而其对立事件只要满足存在性。所以我们考虑先满足所有的单调不减调节,再判断能否满足其对立条件。
我们保存一个数组b,
b
[
i
]
=
=
1
b[i]==1
b[i]==1表示
a
i
+
1
>
=
a
i
a_{i+1}>=a_{i}
ai+1>=ai,反之取0 。对于所有输入的结论,若
t
i
=
=
1
t_i==1
ti==1,则表示其左端点到右端点之间满足单调不减性,将
b
[
l
i
]
b[l_{i}]
b[li]到
b
[
r
i
−
1
]
b[r_{i}-1]
b[ri−1]均置为1 。理论上这个操作可以用线段树完成以实现nlogn的复杂度,但是考虑到数据不大,这里直接用线性表存储即可。而结束之后,构造这样一个数组a:
a
1
=
n
+
1
a_1=n+1
a1=n+1
a
i
=
a
i
−
1
i
f
b
[
i
]
=
1
,
i
>
1
a_i = a_{i-1} \quad if \quad b[i]=1,i>1
ai=ai−1ifb[i]=1,i>1
a
i
=
a
i
−
1
−
1
i
f
b
[
i
]
=
0
,
i
>
1
a_i = a_{i-1}-1 \quad if \quad b[i]=0,i>1
ai=ai−1−1ifb[i]=0,i>1
最后检验所有 t i = = 0 t_i==0 ti==0的区间里是否至少存在一个b==0即可,也可用线段树快速实现检验。
事实上这道题可以考虑改编成一个区间染色+区间查询元素存在性的题目,作为这道题的数据加强版,考察线段树的使用。
以下给出本题代码:
#include<bits/stdc++.h>
using namespace std;
int a[1005];
int c[1005];
vector<pair<int,int> >b;
bool check(int l,int r){
for(int i=l;i<r;++i){
if(a[i]==0)return 1;
}
return 0;
}
int main(){
int n,m,t,l,r;
cin>>n>>m;
while(m--){
cin>>t>>l>>r;
if(t){
for(int i=l;i<r;++i)a[i]=1;
}else{
b.push_back(make_pair(l,r));
}
}
for(auto x:b){
l=x.first,r=x.second;
if(check(l,r)==0){
cout<<"NO"<<endl;
return 0;
}
}
cout<<"YES"<<endl;
c[0]=n;
for(int i=1;i<n;++i){
if(a[i])c[i]=c[i-1];
else c[i]=c[i-1]-1;
}
for(int i=0;i<n;++i)cout<<c[i]<<" ";
cout<<endl;
return 0;
}
D. Subarray Sorting
http://codeforces.com/contest/1187/problem/D
这题是真实线段树题。
问题是给你一个数组,再给你另一个,你每次可以对第一个数组做一次区间排序,问能否变成第二个数组。
首先,每个区间排序,一定可以被有限个二元排序替换。所有我们不妨把操作从区间排序变成相邻的二元排序(冒泡排序的思路)。事实上这题也很像是冒泡排序,你要把你需要的元素一路冒泡的你需要的位置。
那么我们一步步来,先考虑简单的情况。先考虑最后一个元素,如果这两个元素相同,那么可以直接不管他,往左移动一格,考虑一个规模更小的子问题。如果不同,那么我们希望第一个数组中,在不影响其他元素的相对顺序的情况下,从左边调一个需要的元素过来(这不就是冒泡吗?) 。如果能做到,那么就转换成了上一种情况,而如果做不到,那么说明这个问题无解。
这样我们就把问题转换成了一个判断问题:能否从左边调一个元素在不影响原数组顺序的情况下到最右边。首先,考虑冒泡排序的特性,其他元素本来就不会受到影响,所有这个条件直接掠过。接下来只要判断能否移过去就行。这是非常显然的,如果在这段区间内有大于他的元素,那么就过不去,反之一定过得去。
如何判断?维护线段树,保存区间最大值,然后每次都找当前位置往左首次出现大于等于当前元素的位置。如果找到的值大于当前元素,那么就算有这个元素,他也会被挡住,所以返回NO;若找不到,说明根本不存在这个元素,返回NO;若找到且恰好等于,那么我们要的就是这样元素。但是我们模拟把他挪到右边太麻烦了, 所以我们直接把他置为0,表示已经被取过,且不会影响线段树维护的区间最值的性质。而因为我们要找的元素的阈值大于等于1,所以无论如何不会被取到,于是可以实现。
复杂度O(nlogn)
#include<bits/stdc++.h>
using namespace std;
int t[300005<<2];
int a[300005];
int b[300005];
void build(int o,int l,int r){
if(l==r)t[o]=a[l];
else{
int m=(l+r)>>1;
build(o<<1,l,m);
build(o<<1|1,m+1,r);
t[o]=max(t[o<<1],t[o<<1|1]);
}
}
void modify(int o,int l,int r,int x){
if(l==r)t[o]=a[l]=0;
else{
int m=(l+r)>>1;
if(x<=m)modify(o<<1,l,m,x);
else modify(o<<1|1,m+1,r,x);
t[o]=max(t[o<<1],t[o<<1|1]);
}
}
int query(int o,int l,int r,int x){
if(t[o]<x)return -1;
if(l==r)return l;
int m=(l+r)>>1;
int ret=query(o<<1|1,m+1,r,x);
if(~ret)return ret;
return query(o<<1,l,m,x);
}
int T,n;
int main(){
cin>>T;
while(T--){
cin>>n;
for(int i=1;i<=n;++i)scanf("%d",&a[i]);
build(1,1,n);
for(int i=1;i<=n;++i)scanf("%d",&b[i]);
int flag=1;
for(int i=n;i;--i){
int q=query(1,1,n,b[i]);
if(q==-1||a[q]!=b[i]){
flag=0;
break;
}
else modify(1,1,n,q);
}
cout<<(flag?"YES":"NO")<<endl;
}
}
E. Tree Painting
http://codeforces.com/contest/1187/problem/E
出题人号称本题是旋根入门题emmmm结果发现有别的解法导致本题过题数远大于上面一题。
题面是给你一棵树,每次选择一个相邻的节点,然后得到等同于当前节点所在连通块里节点个数的分数,然后把这个节点杀死。问最多得到多少分。第一个节点可以任选。
事实上考虑第一个节点选定以后,结果是唯一确定的。所以问题就是要选定一个根节点,使得答案最大。这个答案的结果有如下两种表示:
1.所有节点到根节点的距离之和。
2.所有节点作为子树根节点得到的树的size的和。
这两个问题似乎是对偶问题,一般而言第一个问题似乎更具实际意义一点。
先考虑简单一点的情况,如果我们已经选定了根节点,如何计算?
不妨使用树形dp,保存两个数组:sz[i]表示以i为根的子树的规模;dp[i]表示以i为根的子树上求出的答案。
有如下转移方程(考虑第二个问题的表述):
s
z
[
i
]
=
1
+
∑
j
∈
c
h
(
i
)
s
z
[
j
]
sz[i]=1+ \sum_{j\in ch(i)}sz[j]
sz[i]=1+j∈ch(i)∑sz[j]
d
p
[
i
]
=
s
z
[
i
]
+
∑
j
∈
c
h
(
i
)
d
p
[
j
]
dp[i]=sz[i]+ \sum_{j\in ch(i)}dp[j]
dp[i]=sz[i]+j∈ch(i)∑dp[j]
其中ch(i)表示i的孩子节点的集合。
两边dfs即可求出值。接下来介绍reroot操作。
观察方程,会发现如果我们把根节点从i转移到j上,事实上会发生改变的数值只有
s
z
[
i
]
,
s
z
[
j
]
,
d
p
[
i
]
,
d
p
[
j
]
sz[i],sz[j],dp[i],dp[j]
sz[i],sz[j],dp[i],dp[j]。只要手动修改这几个数值,就能实现根节点的转移,如此做一遍dfs,保存最大值,就是我们要的答案。考虑父节点为u,子节点为v.
变形如下:
因为dp依赖于sz,将v从u上剪下来,所以有:
d
p
[
u
]
′
=
s
z
[
u
]
′
+
∑
j
∈
c
h
(
u
)
−
v
d
p
[
j
]
dp[u]'=sz[u]'+\sum_{j\in ch(u)-v}dp[j]
dp[u]′=sz[u]′+j∈ch(u)−v∑dp[j]
s
z
[
u
]
′
=
s
z
[
u
]
−
s
z
[
v
]
sz[u]'=sz[u]-sz[v]
sz[u]′=sz[u]−sz[v]
整理得:
d
p
[
u
]
′
=
d
p
[
u
]
−
s
z
[
v
]
−
d
p
[
v
]
dp[u]'=dp[u]-sz[v]-dp[v]
dp[u]′=dp[u]−sz[v]−dp[v]
s
z
[
u
]
′
=
d
p
[
u
]
−
s
z
[
v
]
sz[u]'=dp[u]-sz[v]
sz[u]′=dp[u]−sz[v]
相应的,将u接到v的孩子的位置,v有类似的变换:
d
p
[
v
]
′
=
s
z
[
v
]
′
+
∑
j
∈
c
h
(
v
)
+
u
d
p
[
j
]
dp[v]'=sz[v]'+\sum_{j\in ch(v)+u}dp[j]
dp[v]′=sz[v]′+j∈ch(v)+u∑dp[j]
s
z
[
v
]
′
=
s
z
[
v
]
+
s
z
[
u
]
sz[v]'=sz[v]+sz[u]
sz[v]′=sz[v]+sz[u]
整理得:
s
z
[
v
]
′
=
s
z
[
v
]
+
s
z
[
u
]
sz[v]'=sz[v]+sz[u]
sz[v]′=sz[v]+sz[u]
d
p
[
v
′
]
=
d
p
[
v
]
+
s
z
[
u
]
+
d
p
[
u
]
dp[v']=dp[v]+sz[u]+dp[u]
dp[v′]=dp[v]+sz[u]+dp[u]
得到核心代码段:
void dfs3(LL u,LL fa){
ans=max(dp[u],ans);
for(auto v:g[u]){
if(v==fa)continue;
dp[u]-=dp[v];
dp[u]-=sz[v];
sz[u]-=sz[v];
sz[v]+=sz[u];
dp[v]+=dp[u];
dp[v]+=sz[u];
dfs3(v,u);
dp[v]-=sz[u];
dp[v]-=dp[u];
sz[v]-=sz[u];
sz[u]+=sz[v];
dp[u]+=sz[v];
dp[u]+=dp[v];
}
return;
}
这就是其核心操作,一族关于父子节点的状态参数的变换方程及其逆变换以实现根节点的旋转的效果。其实很有平衡树的感觉。
但是其使用条件也比较明显,就算旋根时改变状态参量的节点不多,比方说本题中是由父子节点受到影响,所以才能以一个常数的代价实现本操作。其复杂度还是dfs的复杂度。
最终复杂度O(n)。
给出完整代码:
#include<bits/stdc++.h>
using namespace std;
#define LL long long
LL n;
vector<LL> g[200005];
LL dp[200005],sz[200005];
LL ans;
LL dfs1(LL u,LL fa){
sz[u]=1;
for(auto v:g[u]){
if(v==fa)continue;
sz[u]+=dfs1(v,u);
}
return sz[u];
}
LL dfs2(LL u,LL fa){
dp[u]=sz[u];
for(auto v:g[u]){
if(v==fa)continue;
dp[u]+=dfs2(v,u);
}
return dp[u];
}
void dfs3(LL u,LL fa){
ans=max(dp[u],ans);
for(auto v:g[u]){
if(v==fa)continue;
dp[u]-=dp[v];
dp[u]-=sz[v];
sz[u]-=sz[v];
sz[v]+=sz[u];
dp[v]+=dp[u];
dp[v]+=sz[u];
dfs3(v,u);
dp[v]-=sz[u];
dp[v]-=dp[u];
sz[v]-=sz[u];
sz[u]+=sz[v];
dp[u]+=sz[v];
dp[u]+=dp[v];
}
return;
}
int main(){
scanf("%lld",&n);
for(LL i=1,x,y;i<n;++i){
scanf("%lld%lld",&x,&y);
g[x].push_back(y);
g[y].push_back(x);
}
dfs1(1,-1);
ans=dfs2(1,-1);
dfs3(1,-1);
cout<<ans<<endl;
}