【JZOJ5679】【GDOI2018Day2模拟4.21】山景城(博弈+树形DP+二分)

Problem

  给定一棵n个节点的树,基德一开始在点s,Kris想让基德进入点t。
  每一秒钟:

  • Kris 在这一秒的开始,可以进行决策:1) 不操作;2) 炸毁一条还存在的走廊,操作过后这条走廊将不复存在;3) 清除一条走廊上被怪盗基德留下标记。
  • 怪盗基德在这一秒的末尾,有可能会移动:假设基德此刻正处于 p 号展厅,若从 p 号展厅出发通往相邻展厅的每条走廊都被画上了标记,那么基德会原地不动;否则,基德会从 p 出发的未被标记的走廊中选择一个,走到对应的展厅,并在这条走廊上留下标记(若存在从 p 出发的未被标记的走廊,基德必须移动,不能选择原地停留)。

(注意:以上过程中 Kris 先决策,决策完了之后基德再会行动)
  双方都会采取最优决策,请求出 Kris 操作的次数。

Hint

subtask1 20pts,n ≤ 10。
subtask2 20pts,保证存在一条 s 到 t 的走廊。
subtask3 20pts,n ≤ 1000。
subtask4 40pts,没有额外限制。 对于100%的数据,1≤t,s,n≤10^6 。

Solution

  这题竟然跟“【JZOJ5388】【GDOI2018模拟9.23】博弈”那道题一毛一样!为什么JZOJ上会放相同的题?!(•́へ•́╬)
  好吧,我比赛的时候感觉之前看过类似的题(没想到就是一样的题),不过依然不会做。况且我大部分时间都花在第二题上(第二题的链接),这题连个正确的暴力都没时间打。

20points:状压DP/dfs

  观察到20points的数据,n非常的小,所以可以上状压DP/dfs。
  设状态S为一个n-1位的三进制数,表示每一条边的状态(0:存在且有标记;1:存在且无标记;2:不存在)。那么我们可以发现,当Kris操作时,只能将某个0变成1或2,或者将某个1变成2。那这样其实已经可以打了。
  但是我们可以发现一个结论:Kris一定是在删好边之后再清标记。也就是说,他会将基德困在某个节点里(当然这之前他要么在删边要么在无所事事),然后再清标记。因为如果你先清标记、后删边,那只会增加基德的选择数,对自己肯定不利,亦即肯定不可能将自己的某个必败态变成自己的必胜态。所以完全可以交换这两次操作。
  那么,我们可以在开始清标记后不再删边,这样会更快一点。
  时间复杂度: O(3nn) O ( 3 n ∗ n )

Code

  都说我没打对了。╮(╯▽╰)╭

40points:树形DP

  在这多出来的20points里,保证有一条s到t的边。
  考虑将t作为根,则s的深度为1(设根的深度为0)。那么此时,其实只有s的子树和t是有用的,t的其他子节点都可以忽略。
  对于基德而言,他肯定是能往下走就往下走,直到不能再走了(亦即被困在某个点),我们就帮他消掉他一路走来做的标记,然后目送着他无可奈何地走向t。
  设基德走到节点u时,最多能够耗掉我们w[u]次操作。那么w[u]的求法可以分类讨论:
  当u为某个叶子节点时,我们根据上述结论,得先删边。首先,我们肯定是删去u到根的路径上的旁边(旁边:路径上所有点通往不在路径上的儿子的边),然后清除所有标记逼着基德走上去。那么这样其实就相当于u到根的路径上的点的出边和。
  当u不为叶子结点时,可以再次分类讨论:
  若u有两个以上的子节点,设big[u]表示w值最大的儿子,sbig[u]为次大的,则我们肯定会在基德走到u时删掉big[u]的那条边,于是w[u]=w[sbig[u]]+?。那么后面那个“?”是1吗?并不是,因为基德走到sbig[u]后,最终他若被困在某个节点,他本来就会删掉经过的路径上的旁边,而big[u]的那条边就包括其中。
  然而,这有一个漏洞:就是假如w[big[u]]=w[sbig[u]],那么我们可以不必删掉u到big[u]的边,随便基德怎么走。这样的话,如果我们在这一秒内不删边,那他最终也要删掉这些旁边;如果我们在这一秒内删掉了以big[u]为根的子树中的一条边,那么基德大可选择走sbig[u],我们还浪费操作次数。所以这个计算方法是正确的。
  若u只有一个子节点,那么Kris有两种操作方式:1.让他走过去;2.删掉那条边,把他困在u。设选择之后,增加的操作数为x。如果他选择方式1,那他最终还是要清掉走过去时留下的标记,而且走过去以后说不定又要耗掉更多的操作,所以x≥1;而方式2的话,仅需删一条边,x=1。于是选择方式2,w[u]=u到根的路径上的点的出边和+1。
  时间复杂度: O(n) O ( n )

Code

  由于我没打,所以下面贴的是zhj大犇的代码(不含暴力):

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int maxN=1e6+10;
bool bz[maxN];
int n,t,s,i,x,y,last[maxN],next[maxN*2],tov[maxN*2],tot,w[maxN],cnt[maxN];
void insert(int x,int y){
    tov[++tot]=y,next[tot]=last[x],last[x]=tot;
}
void dfs(int x){
    int i,y,max=0,maxx=0;
    bz[x]=false;
    for (i=last[x];i;i=next[i]){
        y=tov[i];
        if (bz[y] && y!=t){
            cnt[x]++;
        }
    }
    for (i=last[x];i;i=next[i]){
        y=tov[i];
        if (bz[y] && y!=t){
            cnt[y]=cnt[x];
            dfs(y);
            if (max<w[y])
                maxx=max,max=w[y];
            else if (maxx<w[y])
                maxx=w[y];
        } 
    }
    if (maxx) w[x]=maxx;else w[x]=cnt[x];
}
int main(){
    freopen("mountainview.in","r",stdin);
    freopen("mountainview.out","w",stdout);
    memset(bz,true,sizeof(bz)); 
    scanf("%d%d%d",&n,&t,&s);
    for (i=1;i<n;i++)
        scanf("%d%d",&x,&y),insert(x,y),insert(y,x);
    dfs(s);
    printf("%d",w[s]);
}
100points:树形DP+二分

  依然以t为根。
  设s到t的路径为P,则此时,P可能不止2个点,而且在每个点上都可能挂着一棵子树。如下图所示:
这里写图片描述
  P即为{s,1,2,t}。
  那么这时怎么求w呢?
  以上图的点7为例,Kris需要删的边有5条:4 6、4 8、1 5、2 10、2 11;需要消除标记的边有2条:4 7、1 4。那么,这其实是7到当前它位于的子树的根(这里是点1)的路径上的点的出边和+点1之后的根(不含点t,这里只有点2)的出边和(注意,只是根的出边和)。7到1的路径上的出边共有5条:4 6、4 7、4 8、1 4、1 5;之后的根的出边有2条:2 10、2 11。于是w[7]=7。类似的,w[6]=7,w[8]=7,w[9]=5。
  然后,对于非叶节点u,如果它有两个以上的子节点,也是选次大的;如果只有一个子节点,则和40points的差不多,也是将它当做叶子结点计算w[u],但由于它有一条出边,要删一次,所以再加个1。譬如w[4]=7,而w[5]=5(如果点5是叶子结点,则w[5]=4;但它还有一条出边连向9,所以w[5]=4+1=5)。
  那么,我们可以扫一遍P,然后对于其中的每一个点,我们都以它为根在它们各自的子树内树形DP求出w值。(n为什么这么大,害得我要打人工栈o(╥﹏╥)o
  这时,我们发现直接计算答案不大可能,于是考虑先二分一个答案X。
  如何判断这个X合不合法呢?
  我们沿着P从s扫到t,设当前扫到了第i个根,设Pi为x,y∈son{x},MustMove表示之前和当前必须要删掉的边数,cut表示在当前的子树(即以x为根的子树)内必须要删的边数。则若有某个y满足w[y]+MustMove-cut>X,则x连向y的边必须删。因为MustMove-cut即为之前的子树要删的边,而w[y]表示基德从x走到y后能耗掉我们多少次操作。所以,碰到这种必须要删的边,我们就让MustMove++,cut++。但如果此时MustMove>X(超过答案)||MustMove>i(基德从第1个根s走到了第i个根x,走了i-1步,即操作了i-1次,那么Kris至多能操作i次),则说明此答案过小了。
  时间复杂度: O(nlog2n) O ( n l o g 2 n )

Code
#include <cstdio>
#include <cctype>
using namespace std;
#define fo(i,a,b) for(i=a;i<=b;i++)
#define fd(i,a,b) for(i=a;i>=b;i--)
#define fs(a) for(i=a;i;i=next[i])
const int N=1e6+9,M=N<<1;
int i,n,t,s,u,v,tot,tov[M],next[M],last[N],head,tail,sta[N],x,y,fat[N],cnt,root[N],son[N],
    Sum[N],sum[N],w[N],top,cur[N],exist[N],big[N],sbig[N];

inline void read(int &x)
{
    char ch=getchar();x=0;
    for(;!isdigit(ch);ch=getchar());
    for(;isdigit(ch);x=(x<<3)+(x<<1)+(ch^48),ch=getchar());
}
inline void link(int x,int y)
{
    tov[++tot]=y;
    next[tot]=last[x];
    last[x]=tot;
}
void scan()
{
    read(n);read(t);read(s);
    fo(i,1,n-1)
    {
        read(u);read(v);
        link(u,v);
        link(v,u);
    }
}

void bfs()
{
    head=0;sta[tail=1]=t;
    while(head<tail)
    {
        x=sta[++head];
        fs(last[x])
            if((y=tov[i])!=fat[x])
                fat[sta[++tail]=y]=x,son[x]++;
    }
}

void getpath(int x)
{
    for(;x;x=fat[x])
    {
        root[++cnt]=x;
        if(x!=s)son[x]--;
    }
    int i;
    fd(i,cnt-1,1)Sum[i]=Sum[i+1]+son[root[i]];
}

int rt,cx,S;
void trans(int x,int f)
{
    if(son[f]>1)
    {
        if(w[x]>w[big[f]])sbig[f]=big[f],big[f]=x;
        else
        if(w[x]>w[sbig[f]])sbig[f]=x;
        if(!--exist[f])w[f]=w[sbig[f]];
    }
    else    w[f]=Sum[cx+1]+sum[f]+1;
}
void dp()
{
    int i,x,y;
    sta[top=1]=rt;
    while(top)
    {
        x=sta[top];

        if(!son[x])w[x]=Sum[cx+1]+sum[x];
        else
        {
            fs(cur[x])
                if((y=tov[i])!=fat[x]&&y!=S)
                {
                    sta[++top]=y;
                    cur[x]=next[i];
                    sum[y]=sum[x]+son[x];
                    break;
                }
            if(sta[top]!=x)continue;
        }
        if(x!=rt)trans(x,fat[x]);
        top--;
    }
}
void DP()
{
    int i;
    fo(i,1,n)cur[i]=last[i],exist[i]=son[i];
    fo(i,1,cnt)
    {
        S=i>1?rt:0;
        rt=root[cx=i];
        dp();
    }
}

bool check(int X)
{
    int i,j,x,y,cut,MustMove=0;
    fo(j,1,cnt-1)
    {
        cut=0;S=root[j-1];
        fs(last[x=root[j]])
        {
            y=tov[i];
            if((y=tov[i])!=fat[x]&&y!=S&&w[y]-cut+MustMove>X)
            {
                if(++MustMove>j||MustMove>X)return 0;
                cut++;
            }
        }
    }
    return 1;
}
void Bsort()
{
    int l=0,r=n-1,mid;
    while(l<r)
    {
        mid=l+r>>1;
        if(check(mid))
                r=mid;
        else    l=mid+1;    
    }   
    printf("%d",l);
}

int main()
{
    freopen("mountainview.in","r",stdin);
    freopen("mountainview.out","w",stdout);
    scan();
    bfs();
    getpath(s);
    DP();
    Bsort();
} 
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值