题目链接:hdu 5542
首届CCPC的C题,比赛时一起搞了好久,最后是队友A出的,当时有试过用树状数组来优化 dp,然后今天下午也用树状数组搞了一下午,结果还是踩了和当时一样的坑:我总是把用来记录状态的 dp 数组和树状数组里的内置数组混在一起使用了,而且两重循环的顺序也反了,以至于两组数据
3 2 3 2
1 2 3 和 3 2 1
程序跑都得出了相同的结果,无语。。。之前做 dp 和线段树结合的题时也是傻傻地分不清,说到底就是 dp 的功力很不够,所以在 dp 专场的这场比赛中除了水题外我几乎没什么贡献了,很心塞郁闷了一段时间
题目思路:先建立一个经典的 dp 模型:f[k][i] 表示长度为 k,以 b[i] 结尾的上升子序列的数量(即 dp 的状态),所以递推方程为 f[k][i] = sum{ f[k - 1][x], 1<=x< i && 1<= b[x] <= b[i]-1 },可以看到 k 只依赖于 k-1,而 i 依赖于 b[i] 前面并比 b[i] 小的(和经典的逆序数原理一样),所以可以用树状数组来快速求出,之所以写成 f[k][i] 而不是 f[i][k] 是为了方便树状数组的操作(其实调过来完全可以的,不过脑子一时间转不过弯来,树状数组一直以来都写得太死了,总是模板大法 -_-||)程序实现中还充斥着各种技巧等。
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 #define lowbit(x) ((x) & -(x)) 6 typedef long long ll; 7 const int N = 1003; 8 const ll mod = 1000000007; 9 10 int maxn; 11 ll c[N][N]; // c 数组和 maxn 是树状数组所需的数据结构 12 ll f[N][N]; // f[k][i] 表示长度为 k,以 b[i] 结尾的上升子序列的数量(即 dp 的状态) 13 14 ll sum(int len, int x) { // 树状数组的两个函数 15 ll res = 0; 16 while(x) { 17 res += c[len][x]; // c[][] 的 len 对应 f[][] 的 k,x 对应 f[][] 的 b[i] 18 if(res >= mod) res -= mod; 19 x -= lowbit(x); 20 } 21 return res; 22 } 23 24 void add(int len, int x, ll v) { 25 while(x <= maxn) { 26 c[len][x] += v; 27 if(c[len][x] >= mod) c[len][x] -= mod; 28 x += lowbit(x); 29 } 30 } 31 32 int b[N], a[N]; 33 34 int main() { 35 int t,n,m,Case = 0; 36 scanf("%d",&t); 37 while(t--) { 38 scanf("%d %d",&n,&m); 39 for(int i = 1; i <= n; ++i) { 40 scanf("%d", b + i); 41 a[i] = b[i]; 42 } 43 sort(a + 1, a + n + 1); // 离散化 44 for(int i = 1; i <= n; ++i) 45 b[i] = lower_bound(a + 1, a + n + 1, b[i]) - a; 46 47 maxn = n + 1; 48 memset(c, 0, sizeof c); 49 memset(f, 0, sizeof f); // 记得都要清零 50 51 ll ans = 0; 52 // 两重循环的顺序不能搞错! 53 // 每次对于每个 b[i],都是已经算好了长度 1~min(i,m) 的上升子序列的数量,即 f[1~min(i,m)][i] 的值; 54 // 然后再到 b[i + 1];所以是先枚举 1~n 的离散化后各个元素,再枚举长度(1~min(i,m)) 55 for(int i = 1; i <= n; ++i) { 56 int top = min(i,m); 57 for(int k = 1; k <= top; ++k) { 58 if(k == 1) f[k][i] = 1; 59 else { 60 f[k][i] += sum(k - 1, b[i] - 1); // f[k][i] = sum{f[k - 1][x], 1 <= b[x] <= b[i]-1 } 61 if(f[k][i] >= mod) f[k][i] -= mod; 62 } 63 add(k, b[i], f[k][i]); // 把算好的 f[k][i] 更新进树状数组中去 64 } 65 ans += f[m][i]; // 这时候 f[m][i] 已经算好了,所以加入到答案中 66 if(ans >= mod) ans -= mod; 67 } 68 printf("Case #%d: %I64d ", ++Case, ans); 69 } 70 return 0; 71 }
二重循环中,先固定 i,然后枚举 k,颠倒过来是不行的,原因是如果先固定 k 而枚举 i 的话那么计算完 k-1 后,1~n 的元素都已经进入树状数组中去了,也就是在计算 i 时,i 后面的元素都已经进入树状数组中,这样子就混淆了之前的元素,就好像求逆序数时必须从前往后按顺序来,否则后面比前面的先插入到树状数组中就没意义了。
dp 数组记录状态,然后更新进相应的数据结构中,和 hdu 4521 小明系列问题——小明序列 有相似之处,必须好好体会下 dp 和数据结构结合的这种思想与处理方式。