十分感谢GXZ大佬的讲解,此处致以敬意!emmmm在初学状压DP时就理解了如此精妙的一道题,感到很开森~

\(Address\)


#\(\color{red}{\mathcal{Description}}\)

现在有一个长度为n的随机排列,求它的最长上升子序列长度的期望。

为了避免精度误差,你只需要输出答案模998244353的余数。

#\(\color{red}{\mathcal{Solution}}\)

那么这道题,作为一道显然不是那么可做的题,我们首先来讲一下如何骗分:\(next\_permutation\)枚举全排列,然后\(nlogn\)求一遍长度,最终复杂度大约维持在\(O(n!\times nlogn)\)的水平,看一眼数据规模,好像对于\(n\leq 9\)的数据,在你的常数小的情况下跑出来时没有什么问题的。

\(emmmm\)思考一下数据范围,能够用最暴力的方法骗到分的概率极其的低。

于是我们考虑\(dp\)是否可行。

因为事实上,我们可以看到从左向右推好像不是很可行,于是我们考虑,对于一个排列,我们把数从小到大插入到一个空的数列里面。

那么我们首先令一个\(f_i\)(放心跟程序没啥关系)表示,在当前已经确定的一个序列里面,从左至右第\(i\)个数的最长上升子序列长度。基于这个数组,我们再令\(maxL_i\)表示前缀最小值,即$$maxL_i = max{f_1,f_2...,f_i}$$那么对于这个\(maxL\)数组,显然有$$maxL_i \leq maxL_{i+1} \leq maxL_{i} +1$$

诶,看上去这个\(maxL\)数组更加友善一些,因为我们可以差分它。不妨设对其进行差分的数组为\(dif\)。


回归正题,在我们把数从小到大插入的时候,对于\(dif\)数组会出现如此情况:

考虑在第\(i\)位和第\(i+1\)位之间插入了一个新的数,而因为我们是单调地插入的,所以新插入的这个数一定是当前序列的最大数。那么很显然的是,这个数的\(maxL\)一定是\(maxL_i+1\),因此把\(dif_{i+1}\)改成\(1\),而在\(i\)之后第一个比\(base_i\)大的数,记其位置为\(pos\),则\(dif_{pos}\)值肯定也为\(1\),但是当我们插入了这个新的数之后,由于在它之前刚刚插入了一个不应该加入\(f_{pos}\),所以我们应当把\(dif_{pos}\)置成零。

那么很显然了,我们接下来要做的就是对\(dif\)数组进行状压\(DP\)。

那我们不妨令\(dp_{i,j}\)表示在一个\(1\)~$ i\(的排列里,查分数组\)dif\(状态为\)j$的方案数,那么答案就是 $$ ans=\frac{1}{n!}\sum_{i =0}{2{n-1}-1}{dp_{n,i} \times getlen(i)} $$

也就是\(\sum\)有\(n\)个数、状态为\(i\)的方案\(\times\)方案中的\(LIS\)的长度。

值得一提的是,由于我们状压了\(dif\)数组,所以每个方案中\(LIS\)的长度,就是该状态里\(1\)的个数。

嗯,状压DP就是好啊

呃,什么,你说状态转移方程?我感觉像这种只有两维的状压\(DP\)的方程不都是一个样的吗……

至于代码,有几个\(Tricks\)值得留意:

\(1\)、我们发现其实\(dp\)数组的第一维\(i\)是可以滚掉的,所以我们就滚掉它,因为实际上我们最后的状态数量达到了\(2^{27}\)空间承受不起啊!所以我们就要卡着上限开,并且依旧会爆空间\(OTZ\)

\(2\)、因为一定会有$$maxL_1 = maxL_0+1= 1$$所以我们可以少状压一次。

\(3\)、为了便于递推,我所枚举的状态以及一系列都是跟数组的定义规则相同,跟二进制的定义规则相反。

\(4\)、我们最后由于求的是期望,所以要乘上\(n!\)的逆元,费马小定理求即可。

\(5\)、注意是取反号而不是取非号.因为我们在状压的时候,全0也是状态的一部分,所以我们在从后往前枚举(为了便于计算后面将要被置成的0)时应该到-1停止而不是到0停止。


  1. #include <cstdio>
  2. #include <cstring>
  3. #include <iostream>
  4. #define ll long long
  5. using namespace std ;
  6. const ll mod = 998244353LL ;
  7. ll dp[2][134217730], getlen[134217730] ;
  8. ll Mx, N, i, j, k, ans, fac = 1, now, t, pos ;
  9. inline ll lowbit(ll x){return x & (-x) ; }
  10. inline ll my_pow(ll a, ll b){
  11. ll res = 1 ;
  12. while(b){
  13. if(b & 1)res = (res * a) % mod ;
  14. a = (a * a) % mod ;
  15. b >>= 1 ;
  16. }
  17. return res ;
  18. }
  19. int main(){
  20. cin >> N ; N -- ; dp[0][0] = 1 ; Mx = 1 << N ;
  21. for(now = i = 1; i <= N ; now ^= 1, i ++){
  22. fill(dp[now], dp[now] + (1 << i), 0) ;
  23. for(j = 0; j < (1 << (i - 1)); j ++){
  24. dp[now][j << 1] = (dp[now][j << 1] + dp[now ^ 1][j]) % mod, pos = -1 ;
  25. for(k = i - 1; ~k ; k --){
  26. t = ((j >> k) << (k + 1)) | (1 << k) | (j & ((1 << k) - 1)) ;
  27. if(j & (1 << k)) pos = k ;
  28. if(~pos) t ^= (1 << (pos + 1)) ;
  29. dp[now][t] = (dp[now][t] + dp[now ^ 1][j]) % mod ;
  30. }
  31. }
  32. }
  33. for(i = 1; i < Mx; i ++) getlen[i] = getlen[i - lowbit(i)] + 1 ;
  34. for(i = 0; i < Mx; i ++) ans = ( ans + 1ll * dp[N & 1][i] * (getlen[i] + 1) ) % mod ;
  35. for(i = 1; i <= N + 1 ; i ++) fac = (fac * i) % mod ;
  36. ans = ans * my_pow(fac, mod - 2) % mod ;
  37. cout << ans ;
  38. return 0 ;
  39. }

但是无论如何,写这样一个状压DP,只能得到76分,开\(O2\)的话可以得到80分,是因为最终时间复杂度为\(O(2^n \times n^2)\),空间的话也是大的要死,放在普通的的状压DP里面

已经足够优秀了,但是由于这个题的数据达到了惊人的\(2^{27}\),所以我们最终选择用更前卫的方式解决:

打表

嗯,于是这个题就结束了。其实这个题据说有更优秀的做法,需要用到杨氏矩阵等。而因为本蒟蒻在省队集训的时候走神了,所以并不会杨氏矩阵\(OTZ\)

最后再贴一下打表的代码吧:


  1. #include <cstdio>
  2. #include <iostream>
  3. int N ;
  4. int List[50]={19260817, 1,499122178,2,915057326,540715694,946945688,422867403,451091574,317868537,200489273, 976705134,705376344,662845575,331522185,228644314,262819964,686801362,495111839,947040129,414835038,696340671,749077581,301075008,314644758,102117126,819818153,273498600,267588741} ;
  5. int main(){
  6. std::cin >> N ;
  7. std::cout << List[N] ;
  8. return 0;
  9. }

总结:

woc这真是我做过的最难的题了……啃了好几天吧,窝觉得如果窝不是一个刚刚学状压DP的人的话,也不至于理解起来这么麻烦……

还需要努力啊!

[BJWC2018]最长上升子序列的更多相关文章

  1. 洛谷 P4484 - [BJWC2018]最长上升子序列(状压 dp+打表)

    洛谷题面传送门 首先看到 LIS 我们可以想到它的 \(\infty\) 种求法(bushi),但是对于此题而言,既然题目出这样一个数据范围,硬要暴搜过去也不太现实,因此我们需想到用某种奇奇怪怪的方式 ...

  2. Luogu P4484 [BJWC2018]最长上升子序列

    状压\(DP\)+打表,要命的题目..... 具体思路请参考这位巨佬的博客,本蒟蒻对这道题感到心力交瘁,决定不再作出补充.. 关键的要学习的是:对于排列问题,从左到右处理比较困难的话,考虑从小到大把数 ...

  3. 用python实现最长公共子序列算法(找到所有最长公共子串)

    软件安全的一个小实验,正好复习一下LCS的写法. 实现LCS的算法和算法导论上的方式基本一致,都是先建好两个表,一个存储在(i,j)处当前最长公共子序列长度,另一个存储在(i,j)处的回溯方向. 相对 ...

  4. 动态规划之最长公共子序列(LCS)

    转自:http://segmentfault.com/blog/exploring/ LCS 问题描述 定义: 一个数列 S,如果分别是两个或多个已知数列的子序列,且是所有符合此条件序列中最长的,则 ...

  5. [Data Structure] LCSs——最长公共子序列和最长公共子串

    1. 什么是 LCSs? 什么是 LCSs? 好多博友看到这几个字母可能比较困惑,因为这是我自己对两个常见问题的统称,它们分别为最长公共子序列问题(Longest-Common-Subsequence ...

  6. 动态规划求最长公共子序列(Longest Common Subsequence, LCS)

    1. 问题描述 子串应该比较好理解,至于什么是子序列,这里给出一个例子:有两个母串 cnblogs belong 比如序列bo, bg, lg在母串cnblogs与belong中都出现过并且出现顺序与 ...

  7. LintCode 77: 最长公共子序列

    public class Solution { /** * @param A, B: Two string. * @return: the length of the longest common s ...

  8. 最长下降子序列O(n^2)及O(n*log(n))解法

    求最长下降子序列和LIS基本思路是完全一样的,都是很经典的DP题目. 问题大都类似于 有一个序列 a1,a2,a3...ak..an,求其最长下降子序列(或者求其最长不下降子序列)的长度. 以最长下降 ...

  9. 删除部分字符使其变成回文串问题——最长公共子序列(LCS)问题

    先要搞明白:最长公共子串和最长公共子序列的区别.    最长公共子串(Longest Common Substirng):连续 最长公共子序列(Longest Common Subsequence,L ...

随机推荐

  1. delphi之读写文件的三种方式

    一.Tstrings.Tstringlist procedure TForm1.Button2Click(Sender: TObject); var strlist: TStringList; pat ...

  2. Android BitmapFactory.Options

    public Bitmap inBitmap 如果设置,解码选项“对象的方法,采取将尝试重用这个位图加载内容时. public int inDensity 使用的位图的象素密度. public boo ...

  3. MUI框架-07-HBuilder+夜神安卓模拟器

    MUI框架-07-HBuilder+夜神安卓模拟器 有时候我们在 HBuilder 里面 web 浏览器预览我们的 MUI 项目界面时,总感觉这个 web 浏览器随便拖拉比例,大小可调,但它毕竟是浏览 ...

  4. 理解ASP.NET 5运行时命令:DNVM, DNX, 和DNU

    ASP.NET 5 引入了一个新型的运行时,让我们可以现场交付模式组合式构建应用程序,而不依赖于宿主机上的.NET框架.这种新模式为我们提供了命令行工具(DNVM.DNX.DNU)用于管理我们的.ne ...

  5. 利用Vagrant完成开发环境配置

    作者:astaxie链接:https://github.com/astaxie/go-best-practice/blob/master/ebook/zh/01.0.md著作权归作者所有.商业转载请联 ...

  6. Jmeter入门(一)————线程组配置

    线程组相当于有多个用户,同时去执行相同的一批次任务.每个线程之间都是隔离的,互不影响的.一个线程的执行过程中,操作的变量,不会影响其他线程的变量值. Delay Thread creation unt ...

  7. 使用 docker-machine 管理 Azure 容器虚拟机

    安装 docker-machine 请参见该链接(https://docs.docker.com/machine/install-machine "https://docs.docker.c ...

  8. Java学习---Pinyin4j使用手册

    一般用法 pinyin4j的使用很方便,一般转换只需要使用PinyinHelper类的静态工具方法即可: String[] pinyin = PinyinHelper.toHanyuPinyinStr ...

  9. 【重构.改善既有代码的设计】14、总结&读后感

    14.总结 首先,这是一本太老的书,很多观点已经被固化或者过时了.但核心观点没有问题,虽然大多数观点已经被认为是理所当然的事情了.   重构的定义 重构分几种: 1.狭义的代码重构   就是本书讲的, ...

  10. ZT Android的引用计数(强弱指针)技术及一些问题

    Android的引用计数(强弱指针)技术及一些问题 分类: Android 2013-06-07 18:25 844人阅读 评论(4) 收藏 举报 目录(?)[+] Android C++框架层的引用 ...