2016级算法第三次上机-G.Winter is coming

904 Winter is coming

思路

难题。首先简化问题, \(n\) 个0与 \(m\) 个1排成一列,连续的0不能超过x个,连续的1不能超过y个,求排列方法数。

显然会想到这是动态规划。最快想到的方法是 \(dp[i][j][x][y]\) 表示已经有i个北境兵j个野人参与排列,且末尾有x个连续北境士兵或y个连续野人士兵的方案数。这方法显然是正确的,但是光是 \(dp[200][200][10][10]\) 数组已经十分接近本题内存限制了,保证MLE。状态转移方法是大模拟,四层for循环,每次增加一个人放在最后,讨论各种情况。具体代码可见MLE参考代码,比较好理解。

不过这个方法已经和答案很接近了,只需要稍微优化一下。可以发现第三维和第四维很多空闲的空间被浪费了,我们没有必要用两个维度来分别记录有几个0或1,可以把第四维变成一个标志位,0代表北境军,1代表野人军,而第三维记录最后一段连续的人数,这样空间变成原来的1/6,算是简单的优化了一下,思想并没有变。具体可见优化代码,在此感谢孟尧提供。

本题还可以继续优化,换种方式,dp[i][j][k]:已经有i个北境兵j个野人参与排列,第三维k是标志位(0代表北境军,1代表野人军)的排列方法数。状态转移方程变为:: \(dp[i][j][0]=∑(dp[i-k][j][1])%MOD\) ;其中 \(k∈[1,min(i,x)]\) 。同理, \(dp[i][j][1]=∑(dp[i][j-k][0])%MOD\) ;其中 \(k∈[1,min(j,y)]\) 。相信你很快就能看懂,这里用 \(dp[i-k][j][1]\) 来代表最后有k个0,相当于同时把三四维合并了,巧妙至极。具体可见最优参考代码。

分析

本题不卡时间,卡的是内存。目的是让大家在解决问题的时候有优化的思想(实际的目的是把它从中等题变成难题)。

DP只能意会,不可言传。大家在做DP题的时候一定要理清思路,一般是先不管空间,毕竟以空间换时间,大多数题都是先卡时间再卡空间的。

以本题为例粗略讲解一下DP,以后不会再讲。记住DP具备的两个要素:最优子结构和子问题重叠,见《算法导论》225页。本问题明显备最优子结构,最少的排列数是由多个较短一点的最少排列数组成。DP的多层循环也是有规律的,因为子问题的重叠,你得先把子问题算出来,才能计算更深层的。这里i和j从小到大地计算,保证所加上的都是已经计算过的,才不会出现问题,如果这题加一个dp[i+1][j][1],那明显不对了,因为这个还没有计算过。状态转移方程有时候很微妙,需要一番数学推理。

最优参考代码

//
// Created by AlvinZH on 2017/10/24.
// Copyright (c) AlvinZH. All rights reserved.
//

#include <cstdio>
#include <cstring>
#include <iostream>
#define MOD 1000007
using namespace std;

int n, m, x, y;
int dp[205][205][2];

int main()
{
    while(~scanf("%d %d %d %d", &n, &m, &x, &y))
    {
        memset(dp, 0, sizeof(dp));
        for (int i = 0; i <= x; ++i)
            dp[i][0][0] = 1;
        for (int i = 0; i <= y; ++i)
            dp[0][i][1] = 1;

        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= m; ++j) {
                for (int k = 1; k <= min(i,x); ++k)
                    dp[i][j][0] = (dp[i][j][0] + dp[i-k][j][1]) % MOD;

                for (int k = 1; k <= min(j,y); ++k)
                    dp[i][j][1] = (dp[i][j][1] + dp[i][j-k][0]) % MOD;
            }
        }

        printf("%d\n", (dp[n][m][0] + dp[n][m][1]) % MOD);
    }
}

/* 把第三四维合并,因为我们只要在状态转移的时候保证最后连续一段不超过x或y就好了,第三维用来记录最后连续的是0还是1就好了。
 * dp[i][j][k]:已经有i个北境兵j个野人参与排列,k为标志位(0代表北境军,1代表野人军)的排列方法数。
 */

优化参考代码

/*
 Author: 孟爻(12593)
 Result: AC Submission_id: 403884
 Created at: Sun Nov 12 2017 23:05:10 GMT+0800 (CST)
 Problem: 904   Time: 73    Memory: 11232
*/

#include <cstdio>
#include <cstring>

long f[205][205][15][2];
long M = 1000007;
long n,m,x,y;

int main() {
    while(~scanf("%ld%ld%ld%ld",&n,&m,&x,&y))
    {
        memset(f,0,sizeof(f));
        f[0][0][0][0]=1;
        for(long i=0; i<=n; i++) {
            for(long j=0; j<=m; j++) {
                if(i) {
                    for(long k=1; k<=x; k++)
                        f[i][j][k][0]=(f[i][j][k][0]+f[i-1][j][k-1][0])%M;
                    for(long k=0; k<=y; k++)
                        f[i][j][1][0]=(f[i][j][1][0]+f[i-1][j][k][1])%M;
                }
                if(j) {
                    for(long k=1; k<=y; k++)
                        f[i][j][k][1]=(f[i][j][k][1]+f[i][j-1][k-1][1])%M;
                    for(long k=0; k<=x; k++)
                        f[i][j][1][1]=(f[i][j][1][1]+f[i][j-1][k][0])%M;
                }
            }
        }
        long ans=0;
        for(long k=1; k<=x; k++)
            ans=(ans+f[n][m][k][0])%M;
        for(long k=1; k<=y; k++)
            ans=(ans+f[n][m][k][1])%M;

        printf("%ld\n",ans);
    }
}

MLE代码

//
// Created by AlvinZH on 2017/10/24.
// Copyright (c) AlvinZH. All rights reserved.
//

#include <cstdio>
#include <cstring>
#define MOD 1000007

int n, m, x, y;
int dp[205][205][12][12];

int main()
{
    while(~scanf("%d %d %d %d", &n, &m, &x, &y))
    {
        memset(dp, 0, sizeof(dp));
        dp[0][0][0][0] = 1;
        for (int i = 0; i <= n; ++i) {
            for (int j = 0; j <= m; ++j) {
                for (int k = 0; k <= x; ++k) {
                    for (int l = 0; l <= y; ++l) {
                        if(dp[i][j][k][l] == 0) continue;
                        if(i != n && k != x)//末尾是北境兵
                        {
                            dp[i+1][j][k+1][0] += dp[i][j][k][l];
                            dp[i+1][j][k+1][0] %= MOD;
                        }
                        if(j != m && l != y)//末尾是野人兵
                        {
                            dp[i][j+1][0][l+1] += dp[i][j][k][l];
                            dp[i][j+1][0][l+1] %= MOD;
                        }
                    }
                }
            }
        }
        int ans = 0;
        for (int i = 1; i <= x; ++i)
            ans = (ans + dp[n][m][i][0]) % MOD;
        for (int i = 1; i <= y; ++i)
            ans = (ans + dp[n][m][0][i]) % MOD;

        printf("%d\n", ans);
    }
}

/*
 * dp[i][j][x][y]表示已经有i个北境兵j个野人参与排列,且末尾有x个连续北境士兵或y个连续野人士兵的方案数。
 */
时间: 2024-10-05 21:07:12

2016级算法第三次上机-G.Winter is coming的相关文章

2016级算法第三次上机-C.AlvinZH的奇幻猜想——三次方

905 AlvinZH的奇幻猜想--三次方 思路 中等题.题意简单,题目说得简单,把一个数分成多个立方数的和,问最小立方数个数. 脑子转得快的马上想到贪心,从最近的三次方数往下减,反正有1^3在最后撑着保证减完.不好意思这是错的,因为1,27,64,125...等立方数之间并不是倍数关系,不能构成贪心策略.举个反例:96=64+8+8+8+8=64+27+1+1+1+1+1,答案明显是5,而贪心会算到7. 既然不是贪心,那就是DP了,没毛病.先讲一下常规做法吧,是这样想的:相当于把一个数化成几份

2016级算法第三次上机-B.Bamboo和巧克力工厂

B Bamboo和巧克力工厂 分析 三条流水线的问题,依然是动态规划,但是涉及的切换种类比较多.比较易于拓展到n条流水线的方式是三层循环,外层是第k个机器手,里面两层代表可切换的流水线 核心dp语句:cost[i][k] = min(cost[i][k], cost[j][k-1]+t[j][i]+p[i][k]) 也可以在A题的基础上详细的列出所有可能的路线切割情况然后找到最小值. 注意本题与A题中t的含义不同. 上机时给出的伪代码 //数组从0开始 const int maxx= 510;

2016级算法第三次上机-F.ModricWang的导弹防御系统

936 ModricWang的导弹防御系统 思路 题意即为:给出一个长度为n的序列,求出其最长不降子序列. 考虑比较平凡的DP做法: 令\(nums[i]\) 表示这个序列,\(f[x]\) 表示以第\(x\)个数为结尾的最长的不降子序列的长度,状态转移方程为: \[ f[i]=(\max{f[j]}+1) \;\;\;\;\;\;\; \mbox{when $nums[i]<=nums[j]$}\\] f中的最大值即为答案. 时间复杂度\(O(n^2)\),空间复杂度\(O(n)\) 当然也可

2016级算法第四次上机-G.ModricWang的序列问题 II

1021 ModricWang的序列问题II 思路 此题与上一题区别不是很大,只是增加了一个长度限制,当场通过的人数就少了很多. 大体解题过程与上一题相同.区别在于对\(f[]\) 的操作.没有长度限制的时候,\(f[]\) 的更新策略是立即更新.假设间隔为\(T\),现在由于需要考虑间隔,那么在处理第\(i\) 个元素的时候,就需要看到 前\(i -T\) 个元素生成的\(f[]\) ,而不能受到第\(i-T+1\) 到 \(i-1\) 个元素的干扰.因此,考虑如下操作:每次准备更新\(f[]

2016级算法第六次上机-A.Bamboo之寻找小金刚

Bamboo之寻找小金刚 分析 可以抽象为许多连续线段,分别计数左拐和右拐的个数.考察叉积的基础应用. 假设ABC三点构成一个夹角∠ABC,B就是拐点,AC是辅助形成夹角.考虑线段AB和BC形成的向量 sin∠ABC= (AB * BC)/|AB|*|BC| 两个向量的叉乘除以它们的模 所以叉乘可以判断夹角是否大于180°从而确定转向.当然叉积是有方向的,可以自己选择哪条边在前,只要标准统一即可.每三个点组成一组,遍历,分别计数左拐数和右拐数.具体叉积相关操作可以看<算法导论> 注意 常见的一

2016级算法第六次上机-E.Bamboo之吃我一拳

Bamboo之吃我一拳 分析 当两个点的距离<=d时,才可以出拳,想要使得满足出拳条件的点对最少但不为0 寻找最近点对距离,得到的最近距离能够使得可以出拳的组数最少,因为除了最近点对外其他组合均不符合条件. 在一堆点中找到两个点的距离最小,暴力的O(n^2)计算量很恐怖,可以用分治思想把问题变小: 把平面上的点分为两拨,距离最近的两个点只可能出现在:第一堆,第二堆,和两堆2中各自一个点 分解 想象一条垂直线把所给点集分成两拨:所有的点要么在直线左边,要么在其右边.按x坐标升序排列. 解决 划分后

2016级算法第四次上机-B ModricWang的序列问题

1019 ModricWang的序列问题 思路 此题题意非常清晰,给定一个序列,求出最长上升子序列的长度.从数据规模来看,需要\(O(nlogn)\) 的算法. \(O(nlongn)\) 求最长上升子序列的做法如下: 维护一个数组\(f[]\) ,其中\(f[i]\) 表示当前步骤下长度为i的上升子序列的末尾元素的最小值. 需要注意的是,\(f[i]\) 一定是单调递增的,这个结论十分显然,这里就不做证明了. 使用动态规划思想,对于原序列中的每个元素,都拿去更新一次\(f[]\) .假设当前元

2016级算法第四次上机-E.Bamboo and the Ancient Spell

Bamboo and the Ancient Spell 分析 可能英文读题难度比较大,但是只要看到全大写的 "THE LONGEST COMMON SUBSEQUENCE !"应该就清楚这是考什么的了. 最长公共子序列:可以不连续.序列长度很大时,暴力方法非常费时,这也是一道比较经典的<算法导论>上的动态规划题. 设序列X=和Y=的一个最长公共子序列Z=,则: 若xm=yn,则zk=xm=yn且Zk-1是Xm-1和Yn-1的最长公共子序列: 若xm≠yn且zk≠xm ,则

2017级算法第三次上机-B.SkyLee逛漫展

ALS 一道动态规划最经典的题目 动态规划实质上其实就是表格法,利用表格来记录每个子问题的解. DP所关注的其实是递归 即一个较小问题的解和一个较大问题的状态转移问题. 其次还要关注的其实还是是初始值的设立,这个决定了后续的递推能否顺利的进行. 还有要思考好dp数组所代表的具体的含义 这样在状态转移的过程中 也可以好一点理解. #include <iostream> #include <algorithm> #include <cstring> using namesp