CF1527D - MEX Tree
p
r
o
b
l
e
m
:
problem:
problem:
给出一棵
n
(
n
<
=
2
e
5
)
n(n<=2e5)
n(n<=2e5) 个结点下标从
0
0
0 开始的树,对于
0
<
=
i
<
=
n
0<=i<=n
0<=i<=n求由无序点对
(
u
,
v
)
(u,v)
(u,v) 之间的简单路径满足路径上结点下标
M
E
X
=
i
MEX=i
MEX=i 的无序序对的数量
s
o
l
u
t
i
o
n
:
solution:
solution:
首先,对于
M
E
X
=
i
MEX=i
MEX=i 的定义,显然我们能分析出两个结论:
- 1. 1. 1.要使 M E X = i MEX=i MEX=i 必须满足 [ 0... i − 1 ] [0...i-1] [0...i−1] 这些结点出现在一条链上,并且结点 i i i 不能出现。
- 2. 2. 2. M E X MEX MEX较大的路径必然由 M E X MEX MEX较小的扩展延伸而来。
M E X = i MEX=i MEX=i 的路径似乎很难找,那么我们可以设 a n s i ans_i ansi 代表 M E X > = i MEX>=i MEX>=i 的数量,则 M E X = i MEX=i MEX=i 的数量可以由 a n s i − a n s i + 1 ans_{i}-ans_{i+1} ansi−ansi+1 得到,并且可以发现这是个单调的过程。如果我们设置两个端点指针 l l l 和 r r r 代表路径的两端,每次找到 a n s i ans_i ansi,两个指针都是停在满足要求的极限位置,接着找 a n s i + 1 ans_{i+1} ansi+1,根据结论 2 2 2,我们不可能使指针向根的位置移动,只能不断像外走,这样复杂度与做法难度都得到了保证,接下来思考细节部分。
我们先以 0 0 0 为根进行一次 d f s dfs dfs 得到子树大小和 d f s dfs dfs序 数组,显然 M E X = 0 MEX=0 MEX=0 的数量为 ∑ s z [ v ] ∗ ( s z [ v ] − 1 ) 2 ( f a [ v ] = 0 ) \sum \frac{sz[v]*(sz[v]-1)}2(fa[v]=0) ∑2sz[v]∗(sz[v]−1)(fa[v]=0),同时 a n s 1 = C ( n , 2 ) − ∣ M E X = 0 ∣ ans_1=C(n,2)-|MEX=0| ans1=C(n,2)−∣MEX=0∣。接着我们依次计算 a n s i ans_i ansi,即在当前路径上已有 [ 0... i − 2 ] [0...i-2] [0...i−2] 的情况下,寻找 i − 1 i-1 i−1 号点。我们只需要用 d f s dfs dfs序 判断 i − 1 i-1 i−1 是否在 l , r l,r l,r 的子树下,并且执行 d f s dfs dfs 直到找到 i − 1 i-1 i−1。(注意特殊情况!当 l , i − 1 l,i-1 l,i−1 同时在 r r r 的子树下,但 i − 1 i-1 i−1 结点不在 l l l 子树中的时候显然不成立,我们只需要记录已经走过的点,在 d f s dfs dfs 的时候记录是否走过已经到达的点即可),如果找不存在这样的一条路径,那么显然 a n s j = 0 ( j > = i ) ans_j=0(j>=i) ansj=0(j>=i) 并且 ∣ M E X = i − 1 ∣ = a n s i − 1 |MEX=i-1|=ans_{i-1} ∣MEX=i−1∣=ansi−1。否则 a n s i = s z [ l ] ∗ s z [ r ] ans_i=sz[l]*sz[r] ansi=sz[l]∗sz[r],但是当 l l l 在 r r r 的子树中,也就是 r r r 仍在 0 0 0 号点时才可能出现这种情况。我们要减去 r r r 的包含 l l l 的子树的大小。同时我们应该判断大于 i i i 的结点是否出现在 l , r l,r l,r 的路径中,如果存在那显然这些结点永远不可能作为某条路径的 M E X MEX MEX,我们需要跳过这些结点。我们可以用一个变量 P P P 记录 a n s i ans_i ansi,这样每次答案都是 P − a n s i P-ans_i P−ansi,并把 P = a n s i P=ans_i P=ansi,最后如果 P P P 不为 0 0 0,那么说明存在一条 M E X = n MEX=n MEX=n 的路径。
c o d e : code: code:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=2e5+10;
int n;
int head[maxn],to[maxn<<1],nex[maxn<<1],tot=0;
int sz[maxn],dfn[maxn],f[maxn],cur=0;
ll ans[maxn];
bool vis[maxn],flag;
inline void add(int u,int v){
to[++tot]=v;
nex[tot]=head[u];
head[u]=tot;
}
void dfs(int u,int fa){
sz[u]=1;
dfn[u]=++cur;
f[u]=fa;
for(int i=head[u];i;i=nex[i]){
int v=to[i];
if(v==fa) continue;
dfs(v,u);
sz[u]+=sz[v];
}
}
inline bool check(int fa,int son){
if(dfn[son]>=dfn[fa]&&dfn[son]<=dfn[fa]+sz[fa]-1) return true;
return false;
}
void go(int u,int x){
if(u==x) return;
for(int i=head[u];i;i=nex[i]){
int v=to[i];
if(v==f[u]) continue;
if(check(v,x)){
if(vis[v]) flag=0;
vis[v]=1;
}
go(v,x);
}
}
int main(){
int t;
cin>>t;
while(t--){
cin>>n;
for(int i=1;i<=n;i++) head[i]=ans[i]=vis[i]=0;
vis[1]=flag=1;
tot=cur=0;
int u,v;
for(int i=1;i<n;i++){
scanf("%d %d",&u,&v);
++u,++v;
add(u,v),add(v,u);
}
dfs(1,0);
ll res=0;
for(int i=head[1];i;i=nex[i]){
int v=to[i];
res+=1ll*sz[v]*(sz[v]-1)/2;
}
ans[1]=res;
res=1ll*n*(n-1)/2-ans[1];
int l=1,r=1,idx;
for(int i=head[1];i;i=nex[i]){
int v=to[i];
if(check(v,2)) idx=v;
}
ll temp;
for(int i=2;i<=n;i++){
if(!res) break;
if(check(l,i)){
go(l,i);l=i;
}
else if(check(r,i)){
go(r,i);r=i;
}
else{
ans[i]=res;res=0;break;
}
if(!flag){
ans[i]=res;res=0;break;
}
if(r==1) temp=1ll*(sz[r]-sz[idx])*sz[l];
else temp=1ll*sz[r]*sz[l];
ans[i]=res-temp;
res=temp;
while(vis[i+1]&&i+1<=n) i++;
}
for(int i=1;i<=n;i++){
printf("%lld ",ans[i]);
}
printf("%lld\n",res);
}
return 0;
}
i
d
e
a
:
idea:
idea:
在计算一些路径上计数问题的时候,如果答案类似
s
z
[
u
]
∗
s
z
[
v
]
sz[u]*sz[v]
sz[u]∗sz[v],我们需要考虑是否存在
l
c
a
(
u
,
v
)
=
u
/
v
lca(u,v)=u/v
lca(u,v)=u/v 的情况,因为如果一个节点是祖先,显然算数量的时候要减去另一个结点所在的子树的大小。由于这道题只有两个端点不可能走重复的路径,所以只有某个点在
1
1
1 才可能是另一个点的祖先。
CF1527E - Partition Game
p
r
o
b
l
e
m
:
problem:
problem:
对于数组
t
t
t 的价值
c
o
s
t
cost
cost,定义
c
o
s
t
(
t
)
=
∑
x
∈
s
e
t
(
t
)
l
a
s
t
(
x
)
−
f
i
r
s
t
(
x
)
cost(t)=\sum_{x\in set(t)}last(x)-first(x)
cost(t)=∑x∈set(t)last(x)−first(x),
s
e
t
(
t
)
set(t)
set(t) 代表数组
t
t
t 中的不可重集合,
f
i
r
s
t
(
x
)
,
l
a
s
t
(
x
)
first(x),last(x)
first(x),last(x) 分别代表数组
t
t
t 中
x
x
x 最先出现和最后出现的位置。现在可以将将长度为
n
(
n
<
=
35000
)
n(n<=35000)
n(n<=35000) 的数组
a
a
a 分为至多
k
(
k
<
=
100
)
k(k<=100)
k(k<=100) 个数组,求所有子数组的最小价值和
s
o
l
u
t
i
o
n
:
solution:
solution:
对于这种分成几段的,很容易想到
D
P
DP
DP,我们直接设
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 代表前
i
i
i 个段包含
j
j
j 个元素的最小代价,显然
d
p
[
i
]
[
j
]
=
min
k
<
j
(
d
p
[
i
−
1
]
[
k
]
+
f
[
k
+
1
]
[
j
]
)
dp[i][j]=\min_{k<j}(dp[i-1][k]+f[k+1][j])
dp[i][j]=mink<j(dp[i−1][k]+f[k+1][j]),
f
[
i
]
[
j
]
f[i][j]
f[i][j] 代表子段
[
i
.
.
.
j
]
[i...j]
[i...j] 的花费,复杂度
O
(
n
2
k
)
O(n^2k)
O(n2k) 显然无法通过。但是显然如果转移的集合是一个只增不减并且只有元素个数增多,元素大小不会变大的集合,那么显然我们只需要记录一个最小值遍历
O
(
n
k
)
O(nk)
O(nk) 转移即可。但是这道题转移过程中,这个集合不仅会增大,里面的所有元素值也会增大,我们可以从此入手。可以发现当对比
d
p
(
i
,
j
)
dp(i,j)
dp(i,j) 与
d
p
(
i
,
j
−
1
)
dp(i,j-1)
dp(i,j−1) 这两个状态的转移集合,多增加了一个
d
p
(
i
−
1
,
j
−
1
)
dp(i-1,j-1)
dp(i−1,j−1)。并且由于多了一个元素
a
[
i
]
a[i]
a[i] 放进第
i
i
i 段里,显然会影响到
f
[
k
+
1
]
[
j
]
f[k+1][j]
f[k+1][j]。
具体会影响到所有第
i
−
1
i-1
i−1 段在
l
a
s
t
[
a
[
i
]
]
last[a[i]]
last[a[i]] 之前结束的所有元素(因为如果上一段结尾在上图最左侧红色箭头位置及以前,显然上一个
a
[
i
]
a[i]
a[i] 就存在前
i
−
1
i-1
i−1 段中,那么第
i
i
i 段只有一个
a
[
i
]
a[i]
a[i] 所以不会产生价值。然后
l
a
s
t
[
a
[
i
]
]
last[a[i]]
last[a[i]] 会被包含在第
i
i
i 段中,所以第
i
i
i 段的价值会增加
i
−
l
a
s
t
[
a
[
i
]
]
i-last[a[i]]
i−last[a[i]] )那么综上,显然变成了一个区间修改,区间查询问题,我们用线段树维护即可,复杂度
O
(
n
k
l
o
g
n
)
O(nklogn)
O(nklogn)。线段树不用每次都重建清空,因为每次我们转移查询的区间都是从
1
1
1 个元素开始慢慢增大的,我们手动修改单点修改线段树即可。
i
d
e
a
:
idea:
idea:
虽然做过很多这种可以分成若干连续子序列的题,但是我一直执着于每个序列的价值应该怎么包含,居然练转移方程都没推出来(虽然感觉推出来了基本就写出来了 XD),实际上这种分段的题,一般每段的信息都能包括在转移中。然后这道题用数据结构优化其实还算好想,但是真的写起来细节还是稍微有点难,还有每次第二重循环的最开始的时候要先把
d
p
[
i
−
1
]
[
i
−
1
]
dp[i-1][i-1]
dp[i−1][i−1] 这个状态给记录进去,否则会出现一些问题,wa82一直没看出来这个问题。。
c o d e : code: code:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int maxn=1e5+10;
int n,k;
int a[maxn],last[maxn];
int tree[maxn<<2],lazy[maxn<<2];
int dp[maxn],vis[maxn];
//dp[i][j]=min(dp[i-1][k]+cost[k+1][j]);
void down_lazy(int l,int r,int x){
int mid=l+r>>1;
lazy[x<<1]+=lazy[x];
tree[x<<1]+=lazy[x];
lazy[x<<1|1]+=lazy[x];
tree[x<<1|1]+=lazy[x];
lazy[x]=0;
}
void modify_1(int l,int r,int x,int pos,int k){
if(l==r){tree[x]=k;return;}
int mid=l+r>>1;
if(lazy[x]) down_lazy(l,r,x);
if(pos<=mid) modify_1(l,mid,x<<1,pos,k);
else modify_1(mid+1,r,x<<1|1,pos,k);
tree[x]=min(tree[x<<1],tree[x<<1|1]);
}
void modify_2(int l,int r,int x,int L,int R,int k){
if(L<=l&&r<=R){
lazy[x]+=k;
tree[x]+=k;
return;
}
int mid=l+r>>1;
if(lazy[x]) down_lazy(l,r,x);
if(L<=mid) modify_2(l,mid,x<<1,L,R,k);
if(mid<R) modify_2(mid+1,r,x<<1|1,L,R,k);
tree[x]=min(tree[x<<1],tree[x<<1|1]);
}
int query(int l,int r,int x,int L,int R){
if(L<=l&&r<=R) return tree[x];
int mid=l+r>>1,ans=1e9;
if(lazy[x]) down_lazy(l,r,x);
if(L<=mid) ans=min(ans,query(l,mid,x<<1,L,R));
if(mid<R) ans=min(ans,query(mid+1,r,x<<1|1,L,R));
return ans;
}
int main(){
scanf("%d %d",&n,&k);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++) last[i]=vis[a[i]],vis[a[i]]=i;
memset(dp,0x3f,sizeof dp);
dp[0]=0;
for(int i=1;i<=k;i++){
modify_1(0,n,1,i-1,dp[i-1]);
for(int j=i;j<=n;j++){
if(last[j]>=i) modify_2(0,n,1,i-1,last[j]-1,j-last[j]);
modify_1(0,n,1,j,dp[j]);
dp[j]=query(0,n,1,i-1,j-1);
}
}
cout<<dp[n]<<endl;
return 0;
}
- 2020-07-12
- 2020-07-14
- 2020-07-18
- 2020-08-11