BZOJ 3167: [Heoi2013]Sao

时间:2023-03-10 05:41:39
BZOJ 3167: [Heoi2013]Sao

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