【算法学习笔记】27.动态规划 解题报告 SJTU_OJ 1254 传手绢

1254. 传手绢

Description

活动的时候,老师经常带着同学们一起做游戏。这次,老师带着同学们一起传手绢。

游戏规则是这样的:n个同学站成一个圆圈,其中的一个同学手里拿着手绢,当老师吹哨子时开始传,每个同学可以把手绢传给自己左右的两个同学中的一个(左右任意),当老师在此吹哨子时,游戏停止,此时,拿着手绢的那个同学要给大家表演一个节目。

abc提出一个有趣的问题:有多少种不同的传手绢方法可以使得从abc手里开始传的手绢,传了m次以后,又回到abc手里。两种传手绢方法被视作不同的方法,当且仅当这两种方法中,接到球的同学按接手绢顺序组成的序列是不同的。比如有三个同学1号、2号、3号,并假设abc为1号,手绢传了3次回到abc手里的方式有1->2->3->1和1->3->2->1,共2种。

Input Format

共一行,有两个用空格隔开的整数n,m(3<=n<=30,1<=m<=30)。

Output Foramt

共一行,有一个整数,表示符合题意的方法数。

Sample Input

3 3

Sample Output

2

Hint

40%的数据满足:3<=n<=30,1<=m<=20 100%的数据满足:3<=n<=30,1<=m<=30

此问题很容易让人联想到邻接矩阵乘法含义。wiki了一下,这类问题叫做”传球问题

证明在这里 矩阵乘法确实好理解 但是实际应用还是稍微复杂,

这里先用一个递归方案来把这个问题进行解析。

注意到一个人传球,只能传左一位和右一位,根据这个突破点就可以写成递归函数。

/*分治法
返回 一共n个人 还剩m次传球机会 最终传到第k个人手里的路线数
k是当前拿球的人的编号 1号是最开始拿球的人
*/
int passGame(int k,int n, int m){
    if(m==0)
        //若还剩0次传球机会 整好到了原处 那就返回1 表示原来的路线是对的 否则是0
        return (k==1) ? 1 : 0;
    m--;//开始传球了 所以要减少一次传球机会
    //向+1传的话
    int ans1 = passGame((k==n ? 1 : k+1 ),n,m);
    //向-1传的话
    int ans2 = passGame((k==1 ? n : k-1 ),n,m);

    return ans1+ans2;

}    

分治法

然后调用passGame(1,n,m)即可

会超时,这种分治法的解决方案是从顶至底的。它会有很多重复的计算,所以我们就会想到和分治法路线相反的动态规划。

动态规划的一个核心思想就是将重复子问题提前计算,从而减少计算量。这也就意味着它必须从底向上计算。

在这个背景下,我们可能直觉想到的是设置一个dp[n][m] 其中d[n][m]表示n个人 传m次 回到原点的方案数,

然后想办法建立起 d[n][m]和d[n][m-1]的关系 或者和 d[n-1][m]的关系 或者和d[n-1][m-1]的关系,从而实现把这个问题分成若干个子问题来进行DP。

但是发现找到他们之间的关系,非常困难。

我们试着换一个思路,由分治法的思维,我们知道了一个事情就是 还剩s次传球机会时,球传到第k个人手里的路线数 = 还剩s-1次传球机会时,球传到第k+1个人手里的路线数 + 还剩s-1次传球机会,求传到第k-1个人手里的路线数

PS s-1的原因是最后还要耗费一次传球机会来把球传到k手里 k+1 k-1 只是简单的表示k的左右

那么我们就可以考虑构造一个dp[][]来存储还剩m次机会 传到第k个人手里的路线数目 这个dp方案就是把分治法给颠倒了而已 思路是完全一模一样的

//动态规划算法
int dp[35][35]={0};

/*dp[i][j]表示 传i次球 传到第j个人的路线的个数
起始点和终止点都是为1号人
*/
int dp_game(int n,int m){
    dp[0][1]=1;//传0次 传到1号人手中 路线只有一个
    //开始找状态转移方程
    /*其实想法很简单 就是想d[i][j]是从哪些状态过来的
        d[i][j]应该是d[i-1][j-1]和d[i-1][j+1]来的 因为j只能收到j-1和j+1的球
        而每次传球恰好i+1
    传0次球 只有1号人才能接到球 其他人都是0 不用算

    */
    for (int i = 1; i <= m; ++i){
        for (int j=1; j <= n; ++j){
            dp[i][j] = dp[i-1][j==1 ? n: j-1 ] + dp[i-1][j==n? 1:j+1];
        }
    }
    return dp[m][1];
}

动态规划

时间: 2024-10-06 04:35:34

【算法学习笔记】27.动态规划 解题报告 SJTU_OJ 1254 传手绢的相关文章

由LCS到编辑距离—动态规划入门—算法学习笔记

一切计算机问题,解决方法可以归结为两类:分治和封装.分治是减层,封装是加层. 动态规划问题同样可以用这种思路,分治. 它可以划分为多个子问题解决,那这样是不是用简单的递归就完成了?也许是的,但是这样会涉及太多的不便的操作.因为子问题有重叠! 针对这种子问题有重叠的情况的解决,就是提高效率的关键. 所以动态规划问题可以总结为:最优子结构和重叠子问题. 解决这个子问题的方式的关键就是:memoization,备忘录. 动态规划算法分以下4个步骤: 描述最优解的结构 递归定义最优解的值 按自底向上的方

算法学习笔记 最短路

图论中一个经典问题就是求最短路,最为基础和最为经典的算法莫过于 Dijkstra 和 Floyd 算法,一个是贪心算法,一个是动态规划,这也是算法中的两大经典代表.用一个简单图在纸上一步一步演算,也是很好理解的,理解透自己多默写几次即可记住,机试时主要的工作往往就是快速构造邻接矩阵了. 对于平时的练习,一个很厉害的 ACMer  @BenLin_BLY 说:"刷水题可以加快我们编程的速度,做经典则可以让我们触类旁通,初期如果遇见很多编不出,不妨就写伪代码,理思路,在纸上进行整体分析和一步步的演算

[算法学习笔记]直接插入排序笔记

直接插入排序概念: 带排元素放在elem[0...n-1]中,初始化时,elem[0]自成1个有序区,无序区为elem[1...n-1],从i=1起,到i=n-1,依次将elem[i]插入有序区[0...n-1]中 直接插入排序算法步骤: 1.在当前有序区域R[1,i-1]中查找R[i]的正确插入位置K(1<=K<=i-1) 2.将R[K,i-1]中的记录均向后移动 3.移动后腾出K位置,插入R[i] (最坏)时间复杂度:O(n^2) 空间复杂度:O(1) /// <summary>

算法学习笔记 递归之 快速幂、斐波那契矩阵加速

递归的定义 原文地址为:http://blog.csdn.net/thisinnocence 递归和迭代是编程中最为常用的基本技巧,而且递归常常比迭代更为简洁和强大.它的定义就是:直接或间接调用自身.经典问题有:幂运算.阶乘.组合数.斐波那契数列.汉诺塔等.其算法思想: 原问题可分解子问题(必要条件): 原与分解后的子问题相似(递归方程): 分解次数有限(子问题有穷): 最终问题可直接解决(递归边界): 对于递归的应用与优化,直接递归时要预估时空复杂度,以免出现用时过长或者栈溢出.优化递归就是以

EM算法学习笔记2:深入理解

文章<EM算法学习笔记1:简介>中介绍了EM算法的主要思路和流程,我们知道EM算法通过迭代的方法,最后得到最大似然问题的一个局部最优解.本文介绍标准EM算法背后的原理. 我们有样本集X,隐变量Z,模型参数θ,注意他们3个都是向量,要求解的log似然函数是lnp(X|θ),而这个log似然函数难以求解,我们假设隐变量Z已知,发现lnp(X,Z|θ) 的最大似然容易求解. 有一天,人们发现引入任意一个关于隐变量的分布q(Z),对于这个log似然函数,存在这样一个分解: lnp(X|θ)=L(q,θ

算法学习笔记 KMP算法之 next 数组详解

最近回顾了下字符串匹配 KMP 算法,相对于朴素匹配算法,KMP算法核心改进就在于:待匹配串指针 i 不发生回溯,模式串指针 j 跳转到 next[j],即变为了 j = next[j]. 由此时间复杂度由朴素匹配的 O(m*n) 降到了 O(m+n), 其中模式串长度 m, 待匹配文本串长 n. 其中,比较难理解的地方就是 next 数组的求法.next 数组的含义:代表当前字符之前的字符串中,有多大长度的相同前缀后缀,也可看作有限状态自动机的状态,而且从自动机的角度反而更容易推导一些. "前

八大排序算法学习笔记:冒泡排序

冒泡排序(Bubble Sort,台湾译为:泡沫排序或气泡排序)是一种简单的排序算法. 它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来.走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成.这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端. 算法原理: 比较相邻的元素.如果第一个比第二个大,就交换他们两个. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对.在这一点,最后的元素应该会是最大的数. 针对所有

C++学习笔记27,虚函数的工作原理

C++规定了虚函数的行为,但是将实现交给了编译器的作者. 通常,编译器处理虚函数的方法是给每一个对象添加一个隐藏成员.隐藏成员中保存了一个指向函数地址数组的指针. 这个数组称为虚函数表(virtual function table,vtbl).虚函数表中存储了为类对象进行声明的虚函数的地址. 例如:基类对象包含一个指针,该指针指向基类的虚函数表. 派生类对象包含一个指针,该指针指向一个独立的虚函数表.如果派生类提供了虚函数的新定义,虚函数表将保存新的函数地址. 如果派生类没有重新定义虚函数,虚函

八大排序算法学习笔记:插入排序(一)

插入排序     包括:直接插入排序,二分插入排序(又称折半插入排序),链表插入排序,希尔排序(又称缩小增量排序).属于稳定排序的一种(通俗地讲,就是两个相等的数不会交换位置) . 直接插入排序: 1.算法的伪代码(这样便于理解):     INSERTION-SORT (A, n)             A[1 . . n] for j ←2 to n do key ← A[ j] i ← j – 1 while i > 0 and A[i] > key do A[i+1] ← A[i]