3167: [Heoi2013]Sao

Time Limit: 30 Sec  Memory Limit: 256 MB
Submit: 96  Solved: 36
[Submit][Status][Discuss]

Description

WelcometoSAO(StrangeandAbnormalOnline)。这是一个VRMMORPG,
含有n个关卡。但是,挑战不同关卡的顺序是一个很大的问题。
有n–1个对于挑战关卡的限制,诸如第i个关卡必须在第j个关卡前挑战,或者完成了第k个关卡才能挑战第l个关卡。并且,如果不考虑限制的方向性,那么在这n–1个限制的情况下,任何两个关卡都存在某种程度的关联性。即,我们不能把所有关卡分成两个非空且不相交的子集,使得这两个子集之间没有任何限制。

Input

第一行,一个整数T,表示数据组数。对于每组数据,第一行一个整数n,表示关卡数。接下来n–1行,每行为“i sign j”,其中0≤i,j≤n–1且i≠j,sign为“<”或者“>”,表示第i个关卡必须在第j个关卡前/后完成。

Output

对于每个数据,输出一行一个整数,为攻克关卡的顺序方案个数,mod
1,000,000,007输出。

Sample Input


2
5
0<2
1<2
2<3
2<4
4
0<1
0<2
0<3

Sample Output

4
6

HINT

对于100%的数据有T≤5,1≤n≤1000。

Source

 

[Submit][Status][Discuss]

请先允许我这个SAO脑残粉吐槽一下,下次出题是不是就该叫ALO,GGO什么的了?我封弊者绝不会允许这种事情发生的……

下面是正经的题解——

既然题目里都说了所有的限制关系忽略方向会形成一棵树,那不搞个树形DP就说不过去了不是?

DP[i][j]表示,i的子树(包含i)在满足其内部所有限制条件下,i在这个序列中位于第j个位置的方案数。

发现,其实合并操作只是作用于两个一维数组,和i具体是啥并没什么关系,当然这是题外话,并不影响解题。(我只是想说可以写个 struct Data,然后开个 array<Data, N> 什么的,再定义个运算符 operator + 什么可能会好写好想很多)

然后先考虑暴力的转移方式吧,下面先给出伪代码,再作详细解释。

for i from  to siz[u] do
for j from to siz[v] do
for k from i+j to i+siz[v] do
newDP[u][k] += DP[u][i]*DP[v][j]*G[i-][k-i]*G[siz[u]-i][siz[v]-k+i]
DP[u] = newDP[u]

这是对于一条树边(u,v),意义为u必须出现在v的后面,的合并操作。其中DP数组的意义同之前的介绍,但是需要注意两点:

1.DP[u]是已经合并u节点和其之前已经遍历到的子树后的DP数组。

2.newDP[u]是一个新开的临时数组,因为我们在转移的时候还要用到DP[u]的信息,所以新开一个数组记录转移答案,转移完再赋给DP[u](当然最后你也可以选择通过适当调整转移的顺序省去这个数组,但是这样更节约脑细胞不是吗)。

然后来说一下G[][]数组,这是一个预处理出来的数组,G[i][j]代表把一个长度为i的有序数列和一个长度为j的有序数列合并成一个长度为i+j的有序数列,其中两个数列本来的元素的相对位置不改变。其预处理也很简单——

for i from  to maxSize do
G[][i] =
G[i][] =
for i from to maxSize do
for j from to maxSize do
G[i][j] = G[i-][j] + G[i][j-]

也就是在每一步枚举一下是把第一个序列的下一个元素加入最终序列还是第二个序列的下一个元素加入最终序列,注意序列长度为0也是有意义的。

然后回头看上边的转移代码。

其中i是枚举的u在其原来序列中的位置,j是枚举的v在其原来序列中的位置,k是枚举一下u在最终序列中的位置。我们发现因为之要求u出现在v之后,所以u最终的位置不一定是i+j,可能比i+j更靠后一些,相对的v所在序列的一些v之后的元素可以填充到u之前,这依然是合法的,而且需要通过DP[u][i]和DP[v][j]转移。可以理解那个四项乘法算式的意义为两个子树的方案数之积x把u原来序列中在u之前的元素和v原来序列中现在在u之前的元素组成新的序列的方案数x把u原来序列中在u之后的元素和v原来序列中现在在u之后的元素组成新序列的方案数。

现在已经有了暴力的转移方法,写一写会发现可以水样例,那基本就是正确的喽?然后粗略估计一下,应当过不去N=1000的数据(显然的好嘛!)。考虑优化方法。

显然,这类问题一般可以把k提到j之前什么的(或做一些其他的循环顺序的改变)来改变式子,往往有意想不到的效果。

我们改成先枚举k,再枚举j,推一下新的算式。(某大爷就推错了这一步,23333)

for i from  to siz[u] do
for k from i+ to siz[v]+i do
for j from to k-i do
newDP[u][k] += DP[u][i]*DP[v][j]*G[i-][k-i]*G[siz[u]-i][siz[v]-k+i]

然后发现j的枚举是没必要的,因为对于一定的i和k,G[i-1][k-i]*G[siz[u]-i][siz[v]-k+i]的值是一定的,DP[u][i]的值是一定的,ΣDP[v][j]可以通过维护DP[v][j]的前缀和O(1)查询。

设$sum[v][d]=\sum_{j<=d}{DP[v][j]}$,代码成了这个样子——

for i from  to siz[u] do
for k from i+ to siz[v]+i do
newDP[u][k] += DP[u][i]*sum[v][k-i]*G[][]*G[][]

其中G数组的下标我就省略了,同上面一模一样。

然后证明当前代码的全局复杂度是$O(N^{2})$的。

i的枚举是u之前已经访问到的子树的数量,k的枚举是v子树的数量,相当于枚举了v子树和u之前子树的所有点对(其中一个点在v的子树,一个在u之前子树)。这些点对只会在其LCA处(也就是u)被枚举,所以不会重复枚举。而一个N个点的树,点对数量显然是$O(N^{2})$的,完结撒花~~~

 #include <cstdio>
#include <cstring> const int mxn = ;
const int mxm = ;
const int mod = 1E9 + ; template <class T>
inline void swap(T &a, T &b)
{
T c;
c = a;
a = b;
b = c;
} #define add(a,b) ((((a)+(b))%mod+mod)%mod)
#define mul(a,b,c,d) (1LL*(a)*(b)%mod*(c)%mod*(d)%mod) int n;
int cas;
int tot; int hd[mxn];
int to[mxm];
int nt[mxm]; inline void addEdge(int a, int b)
{
nt[tot] = hd[a], to[tot] = b, hd[a] = tot++;
nt[tot] = hd[b], to[tot] = a, hd[b] = tot++;
} int cal[mxn][mxn]; inline void prework(void)
{
for (int i = ; i < mxn; ++i)
{
cal[i][] = ;
cal[][i] = ;
} for (int i = ; i < mxn; ++i)
for (int j = ; j < mxn; ++j)
cal[i][j] = add(cal[i - ][j], cal[i][j - ]);
} int sz[mxn];
int dp[mxn][mxn];
int sm[mxn][mxn];
int tp[mxn][mxn]; void dfs(int u, int f)
{
sz[u] = dp[u][] = ; for (int i = hd[u], v; ~i; i = nt[i])
if ((v = to[i]) != f)
{
dfs(v, u); memset(tp[u], , sizeof tp[u]); if (i & )
{
for (int j = ; j <= sz[u]; ++j)
if (j < sz[u] + - j)
swap(dp[u][j], dp[u][sz[u] + - j]); for (int j = ; j <= sz[v]; ++j)
if (j < sz[v] + - j)
swap(dp[v][j], dp[v][sz[v] + - j]);
} {
for (int j = ; j <= sz[v]; ++j)
sm[v][j] = add(sm[v][j - ], dp[v][j]); for (int j = ; j <= sz[u]; ++j)
for (int k = j + ; k <= sz[v] + j; ++k)
tp[u][k] = add(tp[u][k], mul(dp[u][j], sm[v][k - j], cal[j - ][k - j], cal[sz[u] - j][sz[v] - k + j]));
} memcpy(dp[u], tp[u], sizeof dp[u]); sz[u] += sz[v]; if (i & )
{
for (int j = ; j <= sz[u]; ++j)
if (j < sz[u] + - j)
swap(dp[u][j], dp[u][sz[u] + - j]);
}
}
} signed main(void)
{
for (prework(), scanf("%d", &cas); cas--; tot = )
{
scanf("%d", &n); memset(hd, -, sizeof hd);
memset(dp, , sizeof dp);
memset(sm, , sizeof sm); for (int i = ; i < n; ++i)
{
int a, b; char c; scanf("%d", &a); do
c = getchar();
while (c != '>' && c != '<'); scanf("%d", &b); if (c == '>')
addEdge(++a, ++b);
else
addEdge(++b, ++a);
} dfs(, ); int ans = ; for (int i = ; i <= n; ++i)
ans = add(ans, dp[][i]); printf("%d\n", ans);
}
}

另外,这题很坑的一点就是样例中的限制关系是不在符号前后加空格的,但是貌似TestInput是有的,所以像下面这么写都会RE。

scanf("%d%c%d", &a, &c, &b);

我就栽了2次,佩服自己的机智能猜到是这么RE的。

@Author: YouSiki

BZOJ 3167: [Heoi2013]Sao的更多相关文章

  1. BZOJ 3167 [Heoi2013]Sao ——树形DP

    BZOJ4824的强化版. 改变枚举的方案,使用前缀和进行DP优化. 然后复杂度就是$O(n^2)$了. #include <map> #include <cmath> #in ...

  2. 3167: [Heoi2013]Sao [树形DP]

    3167: [Heoi2013]Sao 题意: n个点的"有向"树,求拓扑排序方案数 Welcome to Sword Art Online!!! 一开始想错了...没有考虑一个点 ...

  3. [BZOJ 3167][HEOI 2013]SAO

    [BZOJ 3167][HEOI 2013]SAO 题意 对一个长度为 \(n\) 的排列作出 \(n-1\) 种限制, 每种限制形如 "\(x\) 在 \(y\) 之前" 或 & ...

  4. 【BZOJ3167】[HEOI2013]SAO(动态规划)

    [BZOJ3167][HEOI2013]SAO(动态规划) 题面 BZOJ 洛谷 题解 显然限制条件是一个\(DAG\)(不考虑边的方向的话就是一棵树了). 那么考虑树型\(dp\),设\(f[i][ ...

  5. P4099 [HEOI2013]SAO

    P4099 [HEOI2013]SAO 贼板子有意思的一个题---我()竟然没看题解 有一张连成树的有向图,球拓扑序数量. 树形dp,设\(f[i][j]\)表示\(i\)在子树中\(i\)拓扑序上排 ...

  6. P4099 [HEOI2013]SAO(树形dp)

    P4099 [HEOI2013]SAO 我们设$f[u][k]$表示以拓扑序编号为$k$的点$u$,以$u$为根的子树中的元素所组成的序列方案数 蓝后我们在找一个以$v$为根的子树. 我们的任务就是在 ...

  7. 【BZOJ3167/4824】[Heoi2013]Sao/[Cqoi2017]老C的键盘

    [BZOJ3167][Heoi2013]Sao Description WelcometoSAO(StrangeandAbnormalOnline).这是一个VRMMORPG,含有n个关卡.但是,挑战 ...

  8. [HEOI2013]SAO(树上dp,计数)

    [HEOI2013]SAO (这写了一个晚上QAQ,可能是我太蠢了吧.) 题目说只有\(n-1\)条边,然而每个点又相互联系.说明它的结构是一个类似树的结构,但是是有向边连接的,题目问的是方案个数,那 ...

  9. 【做题记录】 [HEOI2013]SAO

    P4099 [HEOI2013]SAO 类型:树形 \(\text{DP}\) 这里主要补充一下 \(O(n^3)\) 的 \(\text{DP}\) 优化的过程,基础转移方程推导可以参考其他巨佬的博 ...

随机推荐

  1. 大数据入门第二十天——scala入门(一)入门与配置

    一.概述 1.什么是scala  Scala是一种多范式的编程语言,其设计的初衷是要集成面向对象编程和函数式编程的各种特性.Scala运行于Java平台(Java虚拟机),并兼容现有的Java程序. ...

  2. test zhenai

    web.Document.InvokeScript("eval",new string[]{"document.getElementById('passwordbt'). ...

  3. 2017-2018-4 20155317《网络对抗技术》EXP3 免杀原理与实践

    2017-2018-4 20155317<网络对抗技术>EXP3 免杀原理与实践 一.问题回答 (1)杀软是如何检测出恶意代码的?杀软是通过代码特征比对得出的,将检查的代码和自己的特征库的 ...

  4. python面试题(四)

    一.数据类型 1.字典 1.1 现有字典 dict={‘a’:24,‘g’:52,‘i’:12,‘k’:33}请按字典中的 value 值进行排序? sorted(dict.items(),key=l ...

  5. C语言与数据库操作入门

    https://blog.csdn.net/flyingqd/article/details/78763652 C语言与数据库操作入门(Win版) 2017年12月10日 17:30:17 阅读数:1 ...

  6. Microsoft Dynamics CRM 常用JS语法(已转成vs2017语法提示)

    背景 最近接触到Microsoft Dynamics CRM的开发.前端js是必不可少的部分,奈何没有一个语法提示,点不出来后续的语句. 在vscode上面搜索插件的时候发现,有一个大神写的插件htt ...

  7. 如何在内网安装compass

    神器compass是肿么用这里不做介绍,因为我也不清楚,可参考官网:http://compass-style.org.这里主要介绍如何在内网安装compass. 首先介绍一般是如何安装compass的 ...

  8. Asp.Net_<asp:RadioButtonList

       <asp:RadioButtonList runat="server" ID="RadioButtonList1"  RepeatDirection ...

  9. Jmeter目录文件讲解

    1.bin:核心可执行文件,包含配置 2.windows启动文件:jmeter.bat mac或linux启动文件:jmeter jmeter-server:mac或linux分布式压测启动文件 jm ...

  10. unity音量设置(同时设置到多个物体上)——引伸语言设置

    在游戏中游戏设置是一个很重要的功能,但是比如语言设置和音量设置分散在很多个物体的组件上,如果每个对应的物体都放到一个链表里,会导致程序雍总难堪,使用事件调用是最好的方式 音量存储类 SoundMana ...