2021-07-11

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...i1] 这些结点出现在一条链上,并且结点 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} ansiansi+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...i2] 的情况下,寻找 i − 1 i-1 i1 号点。我们只需要用 d f s dfs dfs序 判断 i − 1 i-1 i1 是否在 l , r l,r l,r 的子树下,并且执行 d f s dfs dfs 直到找到 i − 1 i-1 i1。(注意特殊情况!当 l , i − 1 l,i-1 l,i1 同时在 r r r 的子树下,但 i − 1 i-1 i1 结点不在 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=i1=ansi1。否则 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 Pansi,并把 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)=xset(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[i1][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,j1) 这两个状态的转移集合,多增加了一个 d p ( i − 1 , j − 1 ) dp(i-1,j-1) dp(i1,j1)。并且由于多了一个元素 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 i1 段在 l a s t [ a [ i ] ] last[a[i]] last[a[i]] 之前结束的所有元素(因为如果上一段结尾在上图最左侧红色箭头位置及以前,显然上一个 a [ i ] a[i] a[i] 就存在前 i − 1 i-1 i1 段中,那么第 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]] ilast[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[i1][i1] 这个状态给记录进去,否则会出现一些问题,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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值