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
6
HINT
对于100%的数据有T≤5,1≤n≤1000。
Source
请先允许我这个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的更多相关文章
- BZOJ 3167 [Heoi2013]Sao ——树形DP
BZOJ4824的强化版. 改变枚举的方案,使用前缀和进行DP优化. 然后复杂度就是$O(n^2)$了. #include <map> #include <cmath> #in ...
- 3167: [Heoi2013]Sao [树形DP]
3167: [Heoi2013]Sao 题意: n个点的"有向"树,求拓扑排序方案数 Welcome to Sword Art Online!!! 一开始想错了...没有考虑一个点 ...
- [BZOJ 3167][HEOI 2013]SAO
[BZOJ 3167][HEOI 2013]SAO 题意 对一个长度为 \(n\) 的排列作出 \(n-1\) 种限制, 每种限制形如 "\(x\) 在 \(y\) 之前" 或 & ...
- 【BZOJ3167】[HEOI2013]SAO(动态规划)
[BZOJ3167][HEOI2013]SAO(动态规划) 题面 BZOJ 洛谷 题解 显然限制条件是一个\(DAG\)(不考虑边的方向的话就是一棵树了). 那么考虑树型\(dp\),设\(f[i][ ...
- P4099 [HEOI2013]SAO
P4099 [HEOI2013]SAO 贼板子有意思的一个题---我()竟然没看题解 有一张连成树的有向图,球拓扑序数量. 树形dp,设\(f[i][j]\)表示\(i\)在子树中\(i\)拓扑序上排 ...
- P4099 [HEOI2013]SAO(树形dp)
P4099 [HEOI2013]SAO 我们设$f[u][k]$表示以拓扑序编号为$k$的点$u$,以$u$为根的子树中的元素所组成的序列方案数 蓝后我们在找一个以$v$为根的子树. 我们的任务就是在 ...
- 【BZOJ3167/4824】[Heoi2013]Sao/[Cqoi2017]老C的键盘
[BZOJ3167][Heoi2013]Sao Description WelcometoSAO(StrangeandAbnormalOnline).这是一个VRMMORPG,含有n个关卡.但是,挑战 ...
- [HEOI2013]SAO(树上dp,计数)
[HEOI2013]SAO (这写了一个晚上QAQ,可能是我太蠢了吧.) 题目说只有\(n-1\)条边,然而每个点又相互联系.说明它的结构是一个类似树的结构,但是是有向边连接的,题目问的是方案个数,那 ...
- 【做题记录】 [HEOI2013]SAO
P4099 [HEOI2013]SAO 类型:树形 \(\text{DP}\) 这里主要补充一下 \(O(n^3)\) 的 \(\text{DP}\) 优化的过程,基础转移方程推导可以参考其他巨佬的博 ...
随机推荐
- 20155209 林虹宇 Exp 8 Web基础
Exp 8 Web基础 Web前端HTML 正常安装.启停Apache kali本机自带apache,上个实验已经使用过,直接使用 查看80端口. 127.0.0.1 编写一个含有表单的html 在浏 ...
- 20155233 《网络对抗》Exp7 网络欺诈技术防范
应用SET工具建立冒名网站 1.要让冒名网站在别的主机上也能看到,需要开启本机的Apache服务,并且要将Apache服务的默认端口改为80,先在kali中使用netstat -tupln |grep ...
- WPF编程,通过Double Animation动态更改控件属性的一种方法。
原文:WPF编程,通过Double Animation动态更改控件属性的一种方法. 版权声明:我不生产代码,我只是代码的搬运工. https://blog.csdn.net/qq_43307934/a ...
- kvm虚拟化一: 图形化的管理方式
1.安装必要工具yum install -y / qemu-kvm //kvm主程序 libvirt //虚拟化服务库 libguestfs-tools //虚拟机系统管理工具 virt-instal ...
- 使用python处理百万条数据分享(适用于java新手)
1.前言 因为负责基础服务,经常需要处理一些数据,但是大多时候采用awk以及java程序即可,但是这次突然有百万级数据需要处理,通过awk无法进行匹配,然后我又采用java来处理,文件一分为8同时开启 ...
- CM005-逆向分析过程(上篇)
前言 005,都说比较变态,很多人给放过去了,但是我还是决定上了它,既然变态就分两篇,上篇先实际说流程,到底应该怎么上它,下篇会告诉逆向分析的过程和方法 准备 [环境和工具] win7/xp虚拟机环境 ...
- Jmeter(四)_16个逻辑控制器详解
循环控制器: 指定其子节点运行的次数,可以使用具体的数值,也可以设置为变量 1:勾选永远:表示一直循环下去 2:如果同时设置了线程组的循环次数和循环控制器的循环次数,那循环控制器的子节点运行的次数为两 ...
- cocos2d-x学习之路(二)——分析AppDelegate和HelloWorldScene文件
这里我们来看一下cocos自动给我们生成的工程里有些什么东西,并且分析一下这些代码的用途,来为我们以后编写cocos程序铺下基础. 这里我建议看我这份随笔的看官先看看cocos官网的快速入门手册,不然 ...
- 关于InfiniBand几个基本知识点解释
文章出处: https://blog.csdn.net/BtB5e6Nsu1g511Eg5XEg/article/details/83629279 公众号 https://blog.csdn.net/ ...
- 高可用Kubernetes集群-14. 部署Kubernetes集群性能监控平台
参考文档: Github介绍:https://github.com/kubernetes/heapster Github yaml文件: https://github.com/kubernetes/h ...