[BZOJ 1063][NOI 2008]道路设计(树形DP)

59 篇文章 0 订阅
41 篇文章 0 订阅

题目链接

http://www.lydsy.com/JudgeOnline/problem.php?id=1063

题面大意

给一个有 n 个结点的有根树,树根为点1,树上有若干条不相交的链,定义一个点的不便利值为它到根节点的路径上不属于链的边的个数,定义整棵树的不便利值为树中点的最大不便利值,求这棵树的最小不便利值,以及取最小值时的方案数。

思路

由题目限制任意两条链不能相交,可以发现这相当于一个点最多只能和它的两个儿子连链边。
显然对于一个点而言,只有可能在它有3个儿子的情况下,它对应的子树的不便利值取max{1,(v便)},在点数 n 相同的情况下,满三叉树的不便利值是最大的,约log3n
一个不严格的证明如下:因为在儿子个数为1或2的情况下,该点到其儿子可以保证有链覆盖,当儿子个数>=3时,该点对应的子树的不便利值取 max{1,(v便)} ,而儿子个数越少,整棵树的深度就能尽量深,因此儿子个数取3时,这个点对应子树的不便利值才能尽量大。以此类推,每个点都有三个儿子(除了叶子节点),故满三叉树的不便利值是最大的。
因此,在数据规模达到极限范围的情况下整棵树的不便利值最多为 log3105,10
10非常小,因此我们可以暴力枚举一个子树的不便利值。
f[u][i][j] 表示对于子树 u ,它的不便利值为i,它与它的 j 个儿子连边的方案数。
可以得到一个初步的DP方程:

这里写图片描述
(以上图片引自“将狼踩尽”(http://www.cnblogs.com/jianglangcaijin/archive/2013/12/06/3462328.html))

其实可以看作一个树上的计数问题。
注意到一个点能往下引多少条链边其实是有限制的,如果该点和其父亲之间的边是链边(如下图a),那么该点只能往下引0或1条链边。反之,该点可以往下引0或1或2条链边(如下图b)。
这里写图片描述
图(a)
这里写图片描述
图(b)
f[i][j][0]的情况下, i 的儿子的不便利值肯定要增加1,因此f[i][j][0]f[x][j1][0...2]转移来的( x i的儿子)
f[i][j][1] 情况下, i 的其中一个儿子(x)的不便利值是不变的,这个儿子往下只能延伸1条链或者不延伸,然后其他的儿子( y )的不便利值要增加1,它们向下走可以引出0条、1条、2条链边
f[i][j][2]情况下, i 的两个儿子(x, y )的不便利值是不变的,它们继续向下走只能连0或1条链边,对于其他的儿子(z),它们的不便利值要加1,它们继续向下走可以引0、1、2条链边。

这个公式看上去很显然,虽然用这个公式来求虽然可行,但是比较麻烦,实际上可以把这个公式化简,对于点 i 的每个儿子x,进行下面的操作(实际上就是将 xi 的贡献一次性加进去):
这里写图片描述
(以上图片引自“将狼踩尽”(http://www.cnblogs.com/jianglangcaijin/archive/2013/12/06/3462328.html))

然后有个细节需要注意:由于是模意义下的运算,再加上DP过程中有乘法操作,因此如果碰到取模以前不是0,取模以后为0的,那么标记它取模以后为模数,防止做完乘法以后,如果本来不取模答案就不是0,取模以后反而变成0了,以后再做乘法,答案还是0,导致WA。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>

#define MAXN 110000

using namespace std;

typedef long long int LL;

int n,m;
LL f[MAXN][11][3],mod;

struct edge
{
    int u,v,next;
}edges[2*MAXN];

int head[MAXN],nCount=0;

void AddEdge(int U,int V)
{
    edges[++nCount].u=U;
    edges[nCount].v=V;
    edges[nCount].next=head[U];
    head[U]=nCount;
}

LL cal(LL x)
{
    if(x&&x%mod==0) return mod;
    return x%mod;
}

void DFS(int u,int fa)
{
    for(int i=0;i<=10;i++) f[u][i][0]=1LL;
    for(int p=head[u];p!=-1;p=edges[p].next)
    {
        int v=edges[p].v;
        if(v==fa) continue;
        DFS(v,u);
        for(int j=0;j<=10;j++) //枚举子树u的最大不便利值
        {
            LL f1=cal(f[v][j][0]+f[v][j][1]);
            LL f2=0;
            if(j) f2=cal(f[v][j-1][0]+f[v][j-1][1]+f[v][j-1][2]); //!!!!!!!
            f[u][j][2]=cal(f[u][j][2]*f2+f[u][j][1]*f1);
            f[u][j][1]=cal(f[u][j][1]*f2+f[u][j][0]*f1);
            f[u][j][0]=cal(f[u][j][0]*f2);
        }
    }
}

int main()
{
    memset(head,-1,sizeof(head));
    scanf("%d%d%lld",&n,&m,&mod);
    for(int i=1;i<=m;i++)
    {
        int u,v;
        scanf("%d%d",&u,&v);
        AddEdge(u,v);
        AddEdge(v,u);
    }
    if(m!=n-1)
    {
        puts("-1");
        puts("-1");
        return 0;
    }
    DFS(1,-1);
    for(int i=0;i<=10;i++) //暴力枚举整个树最大不便利值的最小值
    {
        if(f[1][i][0]+f[1][i][1]+f[1][i][2]>0) //有方案
        {
            printf("%d\n",i);
            printf("%lld\n",(f[1][i][0]+f[1][i][1]+f[1][i][2])%mod);
            return 0;
        }
    }
    return 0;
}
题目描述 有一个 $n$ 个点的棋盘,每个点上有一个数字 $a_i$,你需要从 $(1,1)$ 走到 $(n,n)$,每次只能往右或往下走,每个格子只能经过一次,路径上的数字和为 $S$。定义一个点 $(x,y)$ 的权值为 $a_x+a_y$,求所有满足条件的路径中,所有点的权值和的最小值。 输入格式 第一行一个整数 $n$。 接下来 $n$ 行,每行 $n$ 个整数,表示棋盘上每个点的数字。 输出格式 输出一个整数,表示所有满足条件的路径中,所有点的权值和的最小值。 数据范围 $1\leq n\leq 300$ 输入样例 3 1 2 3 4 5 6 7 8 9 输出样例 25 算法1 (树形dp) $O(n^3)$ 我们可以先将所有点的权值求出来,然后将其看作是一个有权值的图,问题就转化为了在这个图中求从 $(1,1)$ 到 $(n,n)$ 的所有路径中,所有点的权值和的最小值。 我们可以使用树形dp来解决这个问题,具体来说,我们可以将这个图看作是一棵,每个点的父节点是它的前驱或者后继,然后我们从根节点开始,依次向下遍历,对于每个节点,我们可以考虑它的两个儿子,如果它的两个儿子都被遍历过了,那么我们就可以计算出从它的左儿子到它的右儿子的路径中,所有点的权值和的最小值,然后再将这个值加上当前节点的权值,就可以得到从根节点到当前节点的路径中,所有点的权值和的最小值。 时间复杂度 树形dp的时间复杂度是 $O(n^3)$。 C++ 代码 算法2 (动态规划) $O(n^3)$ 我们可以使用动态规划来解决这个问题,具体来说,我们可以定义 $f(i,j,s)$ 表示从 $(1,1)$ 到 $(i,j)$ 的所有路径中,所有点的权值和为 $s$ 的最小值,那么我们就可以得到如下的状态转移方程: $$ f(i,j,s)=\min\{f(i-1,j,s-a_{i,j}),f(i,j-1,s-a_{i,j})\} $$ 其中 $a_{i,j}$ 表示点 $(i,j)$ 的权值。 时间复杂度 动态规划的时间复杂度是 $O(n^3)$。 C++ 代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值