6438. 【GDOI2020模拟01.16】树上的鼠

题目

由于时间过于久远,而且题面本身也很清晰,所以就懒得另外叙述题目大意了(还有思考历程)。

正解

先考虑一条链的情况(长度为奇数,这里的长度是指点的数量):
如果根在中点,先手无论移到哪里,后手都可以移到它的对称点去。
此时先手必败;
如果根不在中点,先手只要一开始移到中点,先手就赢了。
若长度为偶数,就将中间的两个点都看成中点。
先手第一步先移到离根比较远的那个中点上,以后就用一样的策略,每次到达对方的对称点。所以偶数时先手必胜。
然后这就可以推广到一棵树的情况。
可以发现先手必败的情况当且仅当满足以下条件:
树的直径的长度为奇数,并且根是直径的中点。

于是就可以DP了。设\(f_{i,j}\)表示\(i\)为根的子树,最深点的深度为\(j\)的方案数。
发现直接跑这个东西会挂。
改一下定义,将“最深点深度为\(j\)”改成“最深点深度不超过\(j\)”
考虑转移。直接转移还是会挂。
然后就有了这个套路做法:长链剖分,在转移的时候先继承重儿子的信息,再和轻儿子的信息合并。
信息合并的时候,共同有的长度(两块信息的最小长度)上的信息可以暴力做,至于剩下的信息,可以发现就是个区间乘的操作。
线段树?没必要,直接打标记就可以了(有点像差分)。
所以信息合并的时间复杂度是两块信息的最小长度。
合并一次相当于减少了一条重链,总的时间复杂度就是所有重链的长度加起来,也就是\(O(n)\)

至于统计答案,这是有点复杂的,不过可以推。
这里就不详细解释了。


代码

代码可能有点丑,因为信息很多都是用链表来存的。
常数也很大。

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cassert>
#include <list>
#define N 1000010
#define ll long long
#define mo 998244353
ll qpow(ll x,int y){
    ll res=1;
    for (;y;y>>=1,x=x*x%mo)
        if (y&1)
            res=res*x%mo;
    return res;
}
int n;
struct EDGE{
    int to;
    EDGE *las;
} e[N*2];
int ne;
EDGE *last[N];
int q[N],fa[N],len[N],hs[N];
void getq(){
    int head=1,tail=1;
    q[1]=1;
    while (head<=tail){
        int x=q[head++];
        for (EDGE *ei=last[x];ei;ei=ei->las)
            if (ei->to!=fa[x]){
                fa[ei->to]=x;
                q[++tail]=ei->to;
            }
    }
}
list<ll> _data[N*2],*f[N],*tag[N];
int cnt;
void pd(list<ll>::iterator pf,list<ll>::iterator pt,list<ll> *t){
//  assert(pt!=t->end());
    ll tmp=*pt;
    *pf=*pf*tmp%mo;
    *pt=1;
    ++pt;
    if (pt!=t->end())
        *pt=*pt*tmp%mo;
}
ll pro[N],tagp[N],sum[N],ans,all[N];
int main(){
//  freopen("in.txt","r",stdin);
//  freopen("out.txt","w",stdout);
    freopen("tree.in","r",stdin);
    freopen("tree.out","w",stdout);
    scanf("%d",&n);
    for (int i=1;i<n;++i){
        int u,v;
        scanf("%d%d",&u,&v);
        e[ne]={v,last[u]};
        last[u]=e+ne++;
        e[ne]={u,last[v]};
        last[v]=e+ne++;
    }
    getq();
    for (int i=n;i>=1;--i){
        int x=q[i];
        len[x]=1;
        for (EDGE *ei=last[x];ei;ei=ei->las)
            if (len[ei->to]+1>len[x])
                len[x]=len[ei->to]+1,hs[x]=ei->to;
    }
    for (int i=n;i>=2;--i){
        int x=q[i];
        if (!hs[x]){
            f[x]=&_data[++cnt];
            tag[x]=&_data[++cnt];
            f[x]->push_back(1);
            tag[x]->push_back(1);
            continue;
        }
        f[x]=f[hs[x]];
        f[x]->push_front(1);
        tag[x]=tag[hs[x]];
        tag[x]->push_front(1);
        for (EDGE *ei=last[x];ei;ei=ei->las)
            if (ei->to!=fa[x] && ei->to!=hs[x]){
                int y=ei->to;
                auto pfx=f[x]->begin(),pfy=f[y]->begin();
                auto ptx=tag[x]->begin(),pty=tag[y]->begin();
                ll sumx=0,sumy=1;
                pd(pfx,ptx,tag[x]);
                sumx+=*pfx;
                pfx++,ptx++;
                for (int k=1;k<=f[y]->size();++k,++pfx,++pfy,++ptx,++pty){
                    pd(pfx,ptx,tag[x]),pd(pfy,pty,tag[y]);
                    (sumx+=*pfx)%=mo,(sumy+=*pfy)%=mo;
                    *pfx=((sumx*(*pfy)+sumy*(*pfx)-(*pfx)*(*pfy))%mo+mo)%mo;
                }
                if (pfx!=f[x]->end())
                    (*ptx*=sumy)%=mo;
            }
    }
//  return 0;
    for (int i=1;i<=n;++i)
        pro[i]=1,tagp[i]=1;
    int maxd=0;
    for (EDGE *ei=last[1];ei;ei=ei->las){
        int y=ei->to;
        auto pfy=f[y]->begin(),pty=tag[y]->begin();
//      printf("%d ",y);
        for (int k=0;k<f[y]->size();++k,++pfy,++pty){
            pd(pfy,pty,tag[y]);
//          printf("%d ",*pfy);
        }
//      printf("\n");
        maxd=max(maxd,(int)f[y]->size());
        ll s=1;
        pfy=f[y]->begin();
        int k;
        for (k=1;pfy!=f[y]->end();++pfy,++k){
            s=(s+*pfy)%mo;
            pro[k]=pro[k]*s%mo;
            sum[k]=(sum[k]+*pfy*qpow((s-*pfy+mo)%mo,mo-2))%mo;
        }
        tagp[k]=tagp[k]*s%mo;
    }
    pro[0]=1;
    for (int i=1;i<=maxd;++i){
        pro[i]=pro[i]*tagp[i]%mo;
        tagp[i+1]=tagp[i+1]*tagp[i]%mo;
        tagp[i]=1;
        ans=(ans+pro[i]-pro[i-1]-sum[i]*pro[i-1]%mo+mo+mo)%mo;
    }
    ans+=1;
    for (int i=n;i>=1;--i){
        int x=q[i];
        all[x]=1;
        for (EDGE *ei=last[x];ei;ei=ei->las)
            if (ei->to!=fa[x])
                all[x]=all[x]*(all[ei->to]+1)%mo;
    }
    printf("%lld\n",(all[1]-ans+mo)%mo);
    return 0;
}

总结

做这种博弈题的时候,将当前局面转化成“位置一样,但选择变少”是一种比较妙的决策。
对于这种有关链的长度的信息的合并,可以考虑一下长链剖分。

原文地址:https://www.cnblogs.com/jz-597/p/12238814.html

时间: 2024-11-07 23:32:28

6438. 【GDOI2020模拟01.16】树上的鼠的相关文章

【GDOI2020模拟01.16】树上的鼠 (博弈+长链剖分优化dp)

https://gmoj.net/senior/#contest/show/2989/2 思考什么时候先手会赢. 一开始双方都不会希望走到直径的端点上,因为那样对方就可以走直径而使自己输掉. 删掉直径的端点,考虑剩下的树的子问题. 如果又走到端点去了,对面就走到另外一个端点,那我就走到下一层的直径端点去了. 所以大家一直都不想走到直径端点. 一直删,如果最后只剩1一个点,说明先手必败,否则先手必胜. 如果是一条链,就是链的两边的长度不等先手就必胜. 如果是一棵树,考虑随便找一条直径,每次删去它的

【GDOI2020模拟01.16】划愤(nim积+行列式)

https://gmoj.net/senior/#contest/show/2989/1 先考虑n=2时怎么做,打表找规律找了半天找不出来. 赛后才知道这是nim积. 定义\(x?y\)为\(sg(x,y)\). 有一坨性质: \(x,y<2^{2^k},x?y<2^{2^k}\) \(2^{2^k}?2^{2^k}={3 \over 2}2^{2^k}\) 可以把?看做乘法,\(⊕\)(异或)看做加法,所以还有分配律. 求\(x?y(x>y)\),设\(k\)为最大的\(k\)满足\(

6441. 【GDOI2020模拟01.17】小 ω 维护序列

Description Input Output 输出到标准输出流中. 若干行,对于每个操作 1 和操作 5,输出一个数表示答案. Sample Input Sample Input1 5 8 1 2 3 2 1 1 1 3 5 1 5 2 2 4 1 2 4 3 3 4 0 5 1 1 2 1 1 5 Sample Input2 10 15 5 4 3 5 4 1 5 4 3 1 2 8 580347 4 6 503576 1 2 5 5 8 11 1 2 6 4 7 565239 3 6 3

6442. 【GDOI2020模拟01.18】钩子

题目描述 Description Input Output Sample Input Sample Input1 3 1000000007 Sample Input2 4 1000000007 Sample Output Sample Output1 0 1 0 500000004 0 500000004 500000004 0 500000004 Sample Output2 0 500000004 500000004 0 333333336 166666668 166666668 33333

6447. 【GDOI2020模拟01.19】sort

题目 正解 这题一看也是不能写的题目-- 用个平衡树来维护. 平衡树维护的是一个\(Trie\)的序列.每个\(Trie\)表示这段区间中所有数都会排序一遍. 进行各种操作时,首先会将至多两个节点的\(Trie\)分裂.分裂\(Trie\)会新增\(O(\lg n)\)个节点. 然后将整段区间旋到一棵子树内,然后打标记.平衡树和\(Trie\)上都要打标记. 排序是时候将若干个\(Trie\)合并成一个. 由于这些\(Trie\)是带标记的,所以要将标记下传.\(Trie\)树上标记下传时,如果

【GDOI2020模拟01.17】小 ω 玩游戏 (容斥+EGF)

小 ω 正在玩一个游戏. 小 ω 有一个 n 行 m 列的网格,初始每个方格中都有数字 0.她需要执行 q 次操作,每次操作可以选择其中一个方格 (x, y),然后先将第 x 行的数全部 +1,接着将第 y 列的数全部 +1. 小 ω 想知道有多少种执行操作的方式能使最后的网格中有不超过 k 个奇数. 两种方式不同当且仅当存在某一步中选择的方格坐标不同. \(1<=n,m<=2e5,q<=10^{18}\) 考虑行列分开,对行,算出\(f(x)\)表示恰好x行奇数的方案数,对列同理,算出

6439. 【GDOI2020模拟01.17】小 ω 数排列

题目描述 Description Input Output Sample Input Sample Input1 4 10 3 6 2 9 Sample Input2 8 35 3 7 1 5 10 2 11 6 Sample Output Sample Output1 6 [样例 1 解释] 共有 6 个排列符合条件,它们是 (1, 3, 2, 4),(2, 4, 1, 3),(3, 1, 2, 4),(3, 1, 4, 2),(4, 2, 1, 3),(4, 2, 3, 1). Sample

6445. 【GDOI2020模拟01.19】String

题目 正解 一听到正解是NTT,我心态崩了. 我特么知道是NTT都不知道该怎么做!哪里像个卷积了? 好吧,是我孤陋寡闻-- 设两个字符串分别为\(A\)和\(B\) 可以考虑试着计算出\(A\)每个子串和\(B\)的相似度(就是位置相同.字母相同的个数),直接统计就完事了. 看到字符集这么小,就可以对于每个字母分开考虑. 假如\(A_i=B_j\),那么以\(A_{i-j+1}\)开头的子串就有\(1\)的贡献. 这样一来,看起来就很像是个卷积了. 搞完之后将贡献加起来,统计即可. 总结 还是没

6444. 【GDOI2020模拟01.18】树高

题目 正解 这题也不是给人写出来的-- 先不要考虑操作二. 有一种比较新奇的思路:将点的颜色挂在边上,然后搞出个边的连通块. 这些连通块的维护是很舒服的,直接上数据结构即可. 考虑边的连通块和点的连通块的关系. 假如有\(x\)和\(y\)和\(z\)三个点相连,\(x\)为\(y\)父亲,\(y\)为\(z\)父亲. \((x,y)\)和\((y,z)\)的颜色相同,意味着\(y\)和\(z\)的颜色相同. 推广一下,我们可以发现,对于一个边连通块而言,除了根节点(需要特判是不是整棵树的根节点