TC SRM674 DIV2 T3 VampireTreeDiv2

题面

You are a genealogist specializing in family trees of vampires. Vampire family trees differ from human family trees. In particular, there are two ways how a vampire can be “born”:

  1. A living human can be turned into a vampire (the technical term here is “sired”) by an existing vampire. In this case we call the older vampire the master and the newly created vampire the servant of that master.
  2. Two existing vampires can have a vampire child. In this case we call the two original vampires parents and the new vampire their child. Note that there are no restrictions on the two parent vampires. In particular, their genders and their ancestry can be arbitrary. Any two vampires can have a child.

You are now studying one particular family of vampires. These vampires have all been created from a single vampire: the True Ancestor. This special vampire has no master and no parents. There are n vampires in the family. They are numbered 0 through n-1 in the order in which they were born, with vampire 0 being the True Ancestor.
You are given a description of the family tree in the vector s A and B, each with n-1 elements. For each i between 1 and n-1, inclusive, the values A[i-1] and B[i-1] describe either the master or the parents of vampire i. If vampire i has a master then A[i-1] will be the number of its master and B[i-1] will be equal to -1. Otherwise, A[i-1] and B[i-1] will be the numbers of the two parents of vampire i.
In this particular family it is pretty rare for two vampires to have a child. More precisely, there are at most 15 vampires with two parents each.
You would like to select a representative sample of vampires from this family. The sample must have the following properties:

  • You need to select at least one vampire from each master/servant and also from each parent/child relationship.
  • The total number of vampires in your sample must be as small as possible.

Find and return the number of ways in which you can select the sample, modulo 10^9+7.

约定

  • A will contain between 1 and 999 elements, inclusive.
  • A and B will contain the same number of elements.
  • There are no more than 15 elements of B that are not equal to -1.
  • 0A[i]i 0 ≤ A [ i ] ≤ i for all valid i.
  • 1B[i]i − 1 ≤ B [ i ] ≤ i for all valid i.
  • A[i]B[i] A [ i ] ≠ B [ i ] for all valid i.

题意

不解释了,自己读题面吧。

分析

PART 1 构图

首先我们将所有关系构成一张图,为了后面推理方便,我们对于主仆或亲子关系,从主到仆或从父母到孩子连有向边。显然这是一个DAG(有向无环图)。

PART 2 选择个数最少

我们先来考虑选出个数最小的问题,我们令 F[u][0] F [ u ] [ 0 ] u u 节点不选的情况下,至少需要选几个节点;F[u][1]为选 u u 节点的情况下,包括u至少要选几个节点。
因此状态转移方程如下:

F[u][0]=v|<u,v>EF[v][1] F [ u ] [ 0 ] = ∑ v | < u , v >∈ E F [ v ] [ 1 ]

F[u][1]=v|<u,v>Emin(F[v][0],F[v][1])+1 F [ u ] [ 1 ] = ∑ v | < u , v >∈ E m i n ( F [ v ] [ 0 ] , F [ v ] [ 1 ] ) + 1

即如不选当前节点则需要从它出发的边指向的点都要选,若选则这些点是选与不选都没有关系。

PART 3 求方案数

有了刚才求出的最少选择个数,就可以知道求方案的时候我们该如何转移。
我们令 G[u][0] G [ u ] [ 0 ] 为不选择 u u 节点满足条件的方案数,G[u][1]为选择 u u 节点满足条件的方案数。(当不存在这种方案的时候用+表示)
那么转移方程也可以很快列出:

G[u][0]=v|<u,v>EG[v][1] G [ u ] [ 0 ] = ∏ v | < u , v >∈ E G [ v ] [ 1 ]

G[u][1]=v|<u,v>EG[v][0],G[v][1],G[v][0]+G[v][1],ifF[v][0]<F[v][1]ifF[v][1]<F[v][0]ifF[v][0]=F[v][1] G [ u ] [ 1 ] = ∏ v | < u , v >∈ E { G [ v ] [ 0 ] , if F [ v ] [ 0 ] < F [ v ] [ 1 ] G [ v ] [ 1 ] , if F [ v ] [ 1 ] < F [ v ] [ 0 ] G [ v ] [ 0 ] + G [ v ] [ 1 ] , if F [ v ] [ 0 ] = F [ v ] [ 1 ]

但是!这样会出现一个问题!

考虑对于一个子节点,它的方案数会被转移到它的两个父节点,这样,在最终统计时,就产生了重复。
那么要如何去除呢?
最直接的方式就是断开它与其中一个父亲的边。我们指定它只向其中一个父节点转移,断开与另一个父节点的关系。至于这个父节点的选择,随便怎么选都可以。(然而当时实现的时候处于一种神奇的选法选择了离根节点0最远的父亲)

PART 4 新的问题!

我们考虑对于一个孩子节点,如果它被选择,那么它的父亲可选可不选,但如果它没有被选,它的父亲就是必须被选。而我们在转移时这个状态是无法确定的。但是我们注意到孩子节点数量非常少,只有15。我们可以用直接枚举每个孩子的选与不选,使用状态压缩即可。

参考代码

using namespace std;
typedef long long LL;

const int MAXN = 1005;
const int BIT_NGTONE = 0xff;
const int MOD = 1000000007;
const int INF = 0x3f3f3f3f;

struct Edge {
    int to, next;
} E[MAXN << 1];

class VampireTreeDiv2 {
public:
    int countMinSamples( vector <int> A, vector <int> B );
private:
    int N, tot_chldrn;
    void init();    // 初始化
    void add_edge(int u, int v);    // 加边,从u到v
    void dfs(int u);    // 给神奇的选法配套的深搜
    void dp(int u);     // 记忆化DP
};

int last[MAXN], tote, dep[MAXN], chdrn[20], F[MAXN][2], fa[MAXN], flag[MAXN];
LL G[MAXN][2];

int VampireTreeDiv2::countMinSamples(vector <int> A, vector <int> B) {
    N = A.size();
    init();
    int i;
    for (i = 0; i < N; i++)
        if (B[i] == -1) add_edge(A[i], i + 1);
        else chdrn[tot_chldrn++] = i, add_edge(A[i], i + 1), add_edge(B[i], i + 1);
    dfs(0);
    int lim = 1 << tot_chldrn, stat, mini = INF;
    for (i = 0; i < tot_chldrn; i++) fa[chdrn[i] + 1] = dep[A[chdrn[i]]] > dep[B[chdrn[i]]] ? A[chdrn[i]] : B[chdrn[i]];    // 先选好孩子的一个父节点
    LL res = 0;
    for (stat = 0; stat < lim; stat++) {    // stat枚举孩子们选与不选的状态
        memset(F, BIT_NGTONE, sizeof(F));
        memset(G, BIT_NGTONE, sizeof(G));
        for (i = 0; i < tot_chldrn; i++) flag[chdrn[i] + 1] = (stat >> i & 1) + 1;  // 给孩子打上标记,0表示这个节点不是孩子,1表示是不选的孩子,2表示选的孩子
        dp(0);
        if (F[0][0] < mini) res = G[0][0];      // 更新答案,级计数
        else if (F[0][0] == mini) res = (res + G[0][0]) % MOD;
        mini = min(F[0][0], mini);
        if (F[0][1] < mini) res = G[0][1];
        else if (F[0][1] == mini) res = (res + G[0][1]) % MOD;
        mini = min(F[0][1], mini);
    }
    return res;
}

void VampireTreeDiv2::init() {
    tote = tot_chldrn = 0;
    memset(last, 0, sizeof(last));
    memset(flag, 0, sizeof(flag));
    memset(dep, 0, sizeof(dep));
}

void VampireTreeDiv2::add_edge(int u, int v) {
    E[++tote].to = v, E[tote].next = last[u], last[u] = tote;
}

void VampireTreeDiv2::dfs(int u) {
    for (int i = last[u]; i; i = E[i].next)
        if (dep[E[i].to] < dep[u] + 1) {
            dep[E[i].to] = dep[u] + 1;
            dfs(E[i].to);
        }
}

void VampireTreeDiv2::dp(int u) {
    if (F[u][0] != -1) return;      // 如果搜过了就直接返回
    F[u][0] = 0, G[u][0] = G[u][1] = F[u][1] = 1;   // 初始化
    bool fail0 = false, fail1 = false;  // fail0表示该节点不选的状态是否失效,fail1表示该节点选的状态是否失效
    int delta;
    for (int i = last[u]; i; i = E[i].next) {
        int & v = E[i].to;
        dp(v);      // 无论如何先搜一下
        if (!flag[v] || flag[v] && u == fa[v]) {    // 如果是非孩子节点或允许转移的孩子节点(没有被删边)
            if (F[v][1] >= INF || fail0) fail0 = true, F[u][0] = INF, G[u][0] = 0;  // 若存在从非法状态的转移则对应的状态也非法
            else F[u][0] += F[v][1], G[u][0] = G[u][0] * G[v][1] % MOD;     // 否则更新答案
            delta = min(F[v][0], F[v][1]);
            if (delta >= INF || fail1) fail1 = true, F[u][1] = INF, G[u][1] = 0;
            else F[u][1] += delta,
            G[u][1] *= F[v][0] < F[v][1] ? G[v][0] : (F[v][0] == F[v][1] ? (G[v][0] + G[v][1]) % MOD : G[v][1]), G[u][1] %= MOD;
        }
        else if (F[v][1] >= INF) fail0 = true, F[u][0] = INF, G[u][0] = 0;      // 注意删边只是不计数方案,对于当前节点是否可以不选的状态还是需要判断
    }
    if (flag[u] == 2) F[u][0] = INF, G[u][0] = 0;   // 再最后更新非法状态,可以省去很麻烦的特判
    if (flag[u] == 1) F[u][1] = INF, G[u][1] = 0;
}

总结

这是一道非常好的树型DP。首先它在求方案的同时还要保持选出的样本最少,因此我们就不能单纯维护一个方案计数的转移,并且在转移的时候也会出现一些限制。然后,关于方案数的转移,其实没有难么多复杂的组合关系,就只是单纯的乘法原理,因此思考时不要把它想得复杂,造成害怕,想通了其实很简单。最后应该注意一些细节性的东西。最近打DP状态都不太对,要么是方程写不出,要么是边界没控制好。当然不需要急,这些东西还是要见识的DP多了才会慢慢有感觉的;同时建模的方法和思考的方向也要选择正确。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值