第六届蓝桥杯校园选拔赛试题---派遣敢死队 解题报告

时间:2022-09-10 14:20:53
原题 :   
G将军有一支训练有素的军队,这个军队除开G将军外,每名士兵都有一个直接上级(可能是其他士兵,也可能是G将军)。现在G将军将接受一个特别的任务,需要派遣一部分士兵(至少一个)组成一个敢死队,为了增加敢死队队员的独立性,要求如果一名士兵在敢死队中,他的直接上级不能在敢死队中。
请问,G将军有多少种派出敢死队的方法。注意,G将军也可以作为一个士兵进入敢死队。
输入格式
输入的第一行包含一个整数n,表示包括G将军在内的军队的人数。军队的士兵从1至n编号,G将军编号为1。
接下来n-1个数,分别表示编号为2, 3, ..., n的士兵的直接上级编号,编号i的士兵的直接上级的编号小于i。
输出格式
输出一个整数,表示派出敢死队的方案数。由于数目可能很大,你只需要输出这个数除10007的余数即可。
样例输入1
3
1 1
样例输出1
4
样例说明
这四种方式分别是:
1. 选1;
2. 选2;
3. 选3;
4. 选2, 3。
样例输入2
7
1 1 2 2 3 3
样例输出2
40


数据规模与约定
对于20%的数据,n ≤ 20;
对于40%的数据,n ≤ 100;
对于100%的数据,1 ≤ n ≤ 100000。

资源约定:
峰值内存消耗 < 256M
CPU消耗  < 2000ms

请严格按要求输出,不要画蛇添足地打印类似:“请您输入...” 的多余内容。
所有代码放在同一个源文件中,调试通过后,拷贝提交该源码。

注意: main函数需要返回0
注意: 只使用ANSI C/ANSI C++ 标准,不要调用依赖于编译环境或操作系统的特殊函数。
注意: 所有依赖的函数必须明确地在源文件中 #include <xxx>, 不能通过工程设置而省略常用头文件。

提交时,注意选择所期望的编译器类型 

解题思路 :
熟悉树形动态规划的都能看出这是个极度类似于'没有上司的宴会'的题目, 但是这题的规划方向还融合了组合数学的内容, 建议对这题完全没有思路的同学去看一下树形DP和没有上司的宴会, 在这里, 仅详述从子树到原树的状态转移关系.
1. 所有的结点都包含两个成员, beChoosedCount 和 beAbandonCount, 分别代表该结点被选择时带来的方案数 和 被抛弃时带来的方案数, 并且这两个值的初始化都是1, 因为无论如何, 被选是1种情况, 不被选也是1种情况. 比如说, 一个物品放在桌面上, 我们能对它有多少中操作方式呢 ? 大家都知道是2种 : 拿或不拿. 所以, 操作总数 = beChoosedCount + beAbandonCount.
2. 当已知下属子树的操作方案信息, 如何求原树的操作方案信息 ? 按着题意, 原树的根节点代表子树根节点的上司, 而且上司与下属只能选其一, 所以, 我们能有如下的关系式:
上司的beChoosedCount = 累乘( 下属的beAbandonCount );
上司的beAbandonCount = 累乘( 下属的总操作数 ) = 累乘( 下属的beChoosedCount + 下属的beAbandonCount );

为什么是累乘 ? 这属于一个选排列的数学问题, 这里不作详述.
有了上述关系式, 我们就可以写代码了, 存储结构依旧采用有向图的邻接表实现, 而不是一般树的左孩子右兄弟表示法.

#include <iostream>
using namespace std;
#define MOD 10007
class Vertex;
class Edge{ // ...边
public:
Edge *m_next; // ...出度顶点的下一条边
Vertex *m_to; // ...本边指向
Edge():m_next(NULL),m_to(NULL){}
};
class Vertex{ // ...顶点
public:
Edge *m_edge; // ...第一条出度边
int beChooseCount,beAbandonCount; // ...自身被选时的总方案数, 自身不被选时的总方案数
Vertex():m_edge(NULL){beChooseCount = beAbandonCount = 1;}
void Add( Vertex* to, Edge *E){ // ...添加一条指向顶点to的边
E->m_next = m_edge;
E->m_to = to;
m_edge = E;
}
};
class Graph{ // ...图
public:
Vertex *VSet; // ...顶点集
Edge *ESet; // ...边集
int ECount; // ...边的数量
Graph( int N ) {
VSet = new Vertex[N+1];
ESet = new Edge[N];
ECount = 0;
}
inline void Add( int v1, int v2 ){ // ...添加一条从v1到v2的边
VSet[v1].Add( &VSet[v2] , ESet + ECount++ );
}
~Graph(){ delete VSet; delete ESet; }
int GetCount(){ // ...获取总方案数
DFS( &VSet[1] );
return (VSet[1].beAbandonCount + VSet[1].beChooseCount - 1)%MOD; // ...减去一种全空的可能
}
static void DFS( Vertex *V){ // ...搜索顶点V的方案信息
for( Edge *e = V->m_edge; e!=NULL; e=e->m_next ){
DFS( e->m_to ); // ...先求子树信息
// ...若原树结点能被选, 则累乘所有子树结点不能被选的方案数
V->beChooseCount = V->beChooseCount * e->m_to->beAbandonCount %MOD;
// ...若原树结点不能被选, 则累乘所有子树结点的总方案数
V->beAbandonCount = V->beAbandonCount * (e->m_to->beChooseCount+e->m_to->beAbandonCount )%MOD;
}
}
};
int main(int argc, char** argv)
{
int N,superior; // ...士兵数, 上司序号
cin >> N;
Graph G(N);
for( int i = 2; i <= N; G.Add( superior,i++) )
cin >> superior;
cout << G.GetCount();
return 0;
}

算法分析 : 因为每个顶点只遍历一次, 所以整个算法的算法的时间复杂度为O(n), 从存储而言, 只存储了n个顶点和n-1条边, 所以空间复杂度为O(2n).