NOIP2018提高组金牌训练营——动态规划专题
NOIP2018提高组金牌训练营——动态规划专题
https://www.51nod.com/Live/LiveDescription.html#!#liveId=19
多重背包
二进制优化转化成01背包就好了
一只猪走进了一个森林。很凑巧的是,这个森林的形状是长方形的,有n行,m列组成。我们把这个长方形的行从上到下标记为1到n,列从左到右标记为1到m。处于第r行第c列的格子用(r,c)表示。
刚开始的时候猪站在(1,1),他的目标是走到(n,m)。由于猪回家心切,他在(r,c)的时候,只会往(r+1,c)或(r,c+1)走。他不能走出这个森林。
这只猪所在的森林是一个非同寻常的森林。有一些格子看起来非常相似,而有一些相差非常巨大。猪在行走的过程中喜欢拍下他经过的每一个格子的照片。一条路径被认为是漂亮的当且仅当拍下来的照片序列顺着看和反着看是一样的。也就是说,猪经过的路径要构成一个回文。
数一数从(1,1)到(n,m)有多少条漂亮路径。答案可能非常巨大,请输出对 $10^9+7$ 取余后的结果。
样例解释:有三种可能
输入
单组测试数据。 第一行有两个整数 n,m (1≤n,m≤500),表示森林的长和宽。 接下来有n行,每行有m个小写字母,表示每一个格子的类型。同一种类型用同一个字母表示,不同的类型用不同的字母表示。
输出
输出答案占一行。
输入样例
3 4 aaab baaa abba
输出样例
3
注意到500分数据范围,考虑n三方的算法
发现回文非常难判断,可能要记录整个路径,不现实
所以从两端开始搜
f[x1][y1][x2][y2]表示从(1, 1)到(x1,y1),从(n, m)走到(x2, y2)
的方案数。这样就很好转移了,有四个方程,分别对应从(1, 1)往下还是往右
(n, m)往上还是往左
但是发现这样会炸空间,炸时间
那么显然有x1 + y1 = x2 + y2
那么f[x1][y1][x2]可以省去一维
但这样会炸空间
怎么办?
可以这样设计f[step][x1][x2], step = x1 + y1
有什么区别?
这样的话step的这一维只和step-1有关
所以可以用滚动数组优化掉一维的空间。
那么这道题就完美的解决了。
代码略……
1412 AVL树的种类
https://www.51nod.com/Challenge/Problem.html#!#problemId=1412&judgeId=0
平衡二叉树(AVL树),是指左右子树高度差至多为1的二叉树,并且该树的左右两个子树也均为AVL树。 现在问题来了,给定AVL树的节点个数n,求有多少种形态的AVL树恰好有n个节点。
输入
一行,包含一个整数n。 (0 < n <= 2000)
输出
一行表示结果,由于结果巨大,输出它对1000000007取余数的结果。
输入样例
10
输出样例
60
显然状态和节点数和高度有关,
设f[n][d]为有n个节点,高度为d的方案数
那么对于高度d,考虑左右子树,只存在与d-1与d-2两种情况
对于节点数n,可以枚举左右子树分别有多少,有i和n-i-1两种情况
那么转移的方程用到了乘法原理
dp[i][d]= dp[i - j - 1][d - 1] * dp[j][d - 1] + dp[i - j - 1][d - 1] * dp[j][d - 2] + dp[i - j - 1][d - 2] * dp[j][d - 1]
然后注意一些细节,见代码
#include<bits/stdc++.h> #define add(a, b) a = (a + b) % MOD #define REP(i, a, b) for(register int i = (a); i < (b); i++) #define _for(i, a, b) for(register int i = (a); i <= (b); i++) using namespace std; typedef long long ll; const int MAXN = 2e3 + 10; const int MAXM = 15; const int MOD = 1000000007; ll dp[MAXN][MAXM + 10]; int n; int main() { scanf("%d", &n); dp[1][1] = dp[0][0] = 1; //注意这个初始化,特别注意空的节点 _for(i, 2, n) _for(d, 1, MAXM) //深度最大也就十几左右 REP(j, 0, i) //最少0个,最多i-1个 { add(dp[i][d], dp[i - j - 1][d - 1] * dp[j][d - 1] % MOD); add(dp[i][d], dp[i - j - 1][d - 1] * dp[j][d - 2] % MOD); add(dp[i][d], dp[i - j - 1][d - 2] * dp[j][d - 1] % MOD); } ll ans = 0; _for(d, 1, MAXM) add(ans, dp[n][d]); printf("%lld\n", ans); return 0; }
lyk最近计划按顺序做n道题目,每道题目都分为很多分数档次,lyk觉得这些题太简单了,于是它想到了一个好玩的游戏。
lyk决定将每道题目做出其中的某个分数,使得这n道题目的逆序对个数最多。
为了方便,假设共有m个分数档次,并且会给m个分数档次分配一个题目编号,表示该题目会出现这个分数档次。
题目保证每道题都存在至少一个分数档次。(例如样例中5道题目的分数分别是5,6,3,4,7,共有4个逆序对)
输入
第一行两个数n,m(n<=20,m<=100)。 接下来m行,每行一个数ai,表示第ai道题目可能会有i这个分数的档次。
输出
一个数表示最多逆序对个数。
输入样例
5 7 1 2 3 4 1 2 5
输出样例
4
n<=20
反应到状压dp
一般来说,逆序对是按照位置的顺序来求的
但是这道题要逆向思维,按照大小来求
因为题目输入的权值是递增的
所以只要判断在当前位置的后面有多少位置即可
但是这里要注意一点,我一开始用填表法去做,会WA
因为这里状态转移的时候涉及状态本身中1的个数,而有些状态是不合法的。
以前很多题可以用填表法是因为更新状态的时候用到的是状态中的某个位置,然后由位置导出权值,这样即使位置不合法,
导出权值的时候也会不合法而不会导致错解。但这道题就是用到状态本身,使得不合法的状态会导致错解
填表法就会把这些状态算进来。或者加个判断,判断之前的状态要是合法的,但是这显然没有刷表法方便
如果用到刷表法的话,就要注意初始化,起始状态初始化为0,其他为-1,表示不合法
然后转移的时候要判断当前状态合不合法
#include<bits/stdc++.h> #define REP(i, a, b) for(register int i = (a); i < (b); i++) #define _for(i, a, b) for(register int i = (a); i <= (b); i++) using namespace std; int dp[(1 << 20) + 10], n, m; int main() { memset(dp, -1, sizeof(dp)); dp[0] = 0; scanf("%d%d", &n, &m); _for(k, 1, m) { int x; scanf("%d", &x); x--; REP(S, 0, 1 << n) if(dp[S] >= 0 && !(S & (1 << x))) { int sum = 0; REP(j, x + 1, n) if(S & (1 << j)) sum++; dp[S | (1 << x)] = max(dp[S | (1 << x)], dp[S] + sum); } } printf("%d\n", dp[(1 << n) - 1]); return 0; }