北大C++程序设计编程作业答案+解析·魔兽世界之一:备战

本篇文章我们着重讲解本系列课程final大作业的前置作业之一:魔兽世界之一:备战

**招募告示:**想做一个苏菲2炼金的网站,用于计算最优/次优合成图,涉及算法、Web开发、UI/UX等等,有兴趣请私我哒~

1. 前置知识

对于本题所需要的前置知识,大家需要对前几章的知识点有一定的认识,不太明白的童鞋,可以参考前几章编程习题的讲解文章:

  • 从C走进C++
  • 类和对象基础
  • 类和对象提高

2. 思路解析:数据结构

题目描述较长,所以我们先来分析题目要求我们做什么,然后再分析需要什么样的数据结构和算法。这样自顶向下进行程序设计,可以避免面对长篇幅问题,而出现无从下手的窘境。那么通过阅读题目描述,我们可以得知下面几点:

1)我们需要开始一个Wow的游戏;

2)该游戏一开始会启动两个司令(Commander),红司令和蓝司令,并传递初始生命资源给每个司令,并启动每个司令,让其生产战士单位;

3)每个司令按照一定的顺序依次生成战士单位,每生成一个战士会消耗一定的生命资源,当生命资源不足时,则停止生产;

以上三点就能概括题目需要我们做的任务,这里大家无需纠结细节,只需要知道大的框架,在拟定大框架之后,我们在一步一步填充细节。那么,有了这些任务,我们需要什么数据结构呢?首先,我们需要两个类,一个Commander类,相当于生产者,依据生命资源生产相应的战士单位,一个Wow类,相当于游戏管理者,我们只需要启动Wow类,它能初始化Commander,并命令其开始生产。

有了这两个类,那么接下来的问题就是这些类需要有什么成员变量来存储数据,以及需要什么方法来执行生产。我们先来看看需要什么成员变量,先从Wow类开始,一步步递进到Commander类。

Wow类:

  • 时间变量:用于纪录当前时间戳;
  • 红司令和蓝司令:用于启动各自的生产程序;

Commander类:

  • 颜色:当前司令的颜色;
  • 生命资源:纪录当前剩余的生命资源;
  • 每个战士的生命资源消耗列表;
  • 每个战士的名字列表:因为每个司令生成的战士顺序不一致;
  • 每个战士的个数列表:需要纪录每个战士的个数;
  • 战士计数:统计当前一共生成了多少个战士;

3. 代码解析:数据结构

有了类和其数据结构的思路,那么具体的实现是什么样的呢?接下来我们就来看看代码如何实现,首先是Wow类:

class WorldOfWarcraft {
    const static int WARRIOR_NAMES_RED[ WARRIOR_NUM ];
    const static int WARRIOR_NAMES_BLUE[ WARRIOR_NUM ];

    // 时间变量
    unsigned int c = 0; // timestamp count
    // 红司令和蓝司令
    Commander comm_red = Commander( red, WARRIOR_NAMES_RED ); // red headquarter
    Commander comm_blue = Commander( blue, WARRIOR_NAMES_BLUE ); // blue headquarter

    static std::string getTimeStr( unsigned int c );

public:
    // 方法暂略
};

这里还有两个额外的变量:WARRIOR_NAMES_RED 和 WARRIOR_NAMES_BLUE,它们分别纪录了每个司令生成战士的顺序,注意题目中每个司令生成战士的顺序是不同的:

红方司令部按照iceman、lion、wolf、ninja、dragon的顺序循环制造武士。

蓝方司令部按照lion、dragon、ninja、iceman、wolf的顺序循环制造武士。

但为什么他们的数据类型是int不是string,这里我们留到解析成员方法的时候再来揭晓哒~接下来是Commander类:

class Commander {
    const static std::string WARRIOR_NAMES[ WARRIOR_NUM ]; // global warrior names
    const static char* OUTPUT_FORMAT;

    // 颜色
    const Color c; // color
    // 生命资源
    int m; // total life points
    // 每个战士的名字列表
    const int *W_n; // local warrior names
    // 每个战士的生命资源消耗列表
    const unsigned *M; // Warrior life points
    // 战士计数
    unsigned int n = 0; // warrior total count
    // 每个战士的个数列表
    unsigned int C[ WARRIOR_NUM ] = { 0 }; // warrior count
    int idx = -1; // warrior index
    bool is_stopped = false;

public:
   // 方法暂略
};

同样,这里还有四个额外的变量:WARRIOR_NAMES, OUTPUT_FORMAT, idx和is_stopped,其中WARRIOR_NAMES列举了每一个战士的名字,这个内容对于每个司令都是固定的:

武士一共有 dragon 、ninja、iceman、lion、wolf 五种

OUTPUT_FORMAT用于printf()函数输出相应的输出语句,至于idx和is_stopped我们还是依旧留到函数解析的时候在揭晓答案。同时,为了更好的区别颜色和战士,我们声明两个enum类来表示他们:

enum Color {
    red,
    blue
};

enum Warrior {
    dragon, // = 0
    ninja, // = 1
    iceman, // = 2
    lion, // = 3
    wolf // = 4
};

这里需要注意,在没有显性地为每个变量声明值的情况下,变量的值会默认从0开始,具体的例子可以参考上面枚举类Warrior,我标注了每个成员的默认值,这里非常重要,敲黑板哒!

3. 思路解析:算法/成员函数

有了数据结构,接下来我们来分析每个类需要实现的成员函数,也就是说他们需要干什么。首先对于Wow类,它只有一个成员函数start():

class WorldOfWarcraft {
    // 成员变量暂略

public:
    /**
     * Start the game.
     *
     * @param n Case number.
     * @param m Total life points.
     * @param M Array to indicate life points to generate a warrior.
     * */

    void start( unsigned int n, unsigned int m, const unsigned int *M );
};

这个start()函数就是启动整个游戏的入口,对于本题,它只会启动两个司令进行生成工作,同时它有三个参数:

n:测试案例编号;

m:每个司令总的生命资源值;

M:每个司令生成每个战士所需生命资源列表;

在开始游戏之后,那Commander类需要做什么事情呢?

class Commander {
    // 成员变量暂略

public:
    Commander( Color c, const int *W ) : c( c ), W_n( W ) {}

    /**
     * Can this commander generate next warrior?
     *
     * @return An index to that warrior is returned if it can, otherwise -1 is returned.
     * */

    int hasNext();

    /**
     * Generate a warrior if this commander can do it.
     *
     * @param t Timestamp string in the format of "XXX"
     * */

    void generate( std::string &t );

    /**
     * Initialize resources.
     *
     * @param m Total life points.
     * @param M Array to indicate life points to generate a warrior.
     * */

    void initResource( int m, const unsigned *M );
};

除去构造函数,它一共有三个成员函数:

hasNext():检查当前司令是否还有生命资源用于生产;

generate():生产一个战士,并输出相应结果;

initResource:初始化资源,比如总的生命资源值;

总的来说,Wow类需要做的就是,启动两个司令,并告诉他们生产相关的信息,而Commander类需要做的就是,根据所要求的资源信息,如果还有资源剩余,每一个时刻生产相应的战士。在理解上面概念之后,我们就可以具体分析每个类成员函数的实现了。

4. 代码解析:算法/成员函数

为了遵循自定向下的程序设计思路,我们先来看看整体代码的入口函数,也就是我们需要一个函数去初始化Wow类,并启动它,之后才会涉及上面我们讲解的内容。这里我们定义一个入口函数caller():

// -------------------------------------------------------
// Entry function
// -------------------------------------------------------

void caller() {
    int n, m;
    unsigned int M[ WARRIOR_NUM ];
    WorldOfWarcraft wow = WorldOfWarcraft();

    // 读取测试案例数n
    std::cin >> n;
    for ( int i = 1; i <= n; i++ ) {
        std::cin >> m;
        
        // 读取每个测试案例的M
        for ( unsigned int &j : M ) {
            std::cin >> j;
        }

        // 启动游戏进行生产
        wow.start( i, m, M );
    }
}

这个入口函数做的事情很简单,先读取测试案例数n,然后读取每个测试案例的M,也就是当前生产战士的资源消耗列表,之后就是启动游戏进行生产。

4.1 Wow类

接下来是Wow类的成员函数start():

void WorldOfWarcraft::start( unsigned int n, unsigned int m, const unsigned int *M  ) {
    std::cout << "Case:" << n << std::endl;

    // 对每个司令进行资源分配
    c = 0;
    comm_red.initResource( m, M );
    comm_blue.initResource( m, M );

    std::string s;
    // 循环询问是否还有司令有生产能力,有则进行生产
    while ( comm_red.hasNext() >= 0 || comm_blue.hasNext() >= 0 ) {
        s = getTimeStr( c );
        comm_red.generate( s );
        comm_blue.generate( s );
        c++;
    }

    // 打印结束生产的语句
    s = getTimeStr( c );
    comm_red.generate( s );
    comm_blue.generate( s );
}

这个函数也很简单,首先对每个司令进行资源分配,然后循环询问是否还有司令有生产能力,有则进行生产。但是这里注意在while循环结束后,还需要调用一次生产语句,有可能有小伙伴要问,这是为什么呢?这是因为我们需要打印结束生产的语句,当while结束的时候,我们只知道这个时候结束生产了,但是我们还需要打印一下相关结果。这么做的结果当然是我们将生产语句都放在Commander类的generate()函数中了。最后还一个额外的私有成员函数getTimeStr():

std::string WorldOfWarcraft::getTimeStr( unsigned int c ) {
    std::string s = std::to_string( c );
    assert( s.size() < 4 );

    switch ( s.size() ) {
        // e.g. 1 => 001
        case 1:
            s.insert( 0, 2, '0' );
            break;
        // e.g. 22 => 022
        case 2:
            s.insert( 0, 1, '0' );
            break;
    }

    return s;
}

这个函数是根据当前时间戳生产相应的时间字符串的,也就是在前面补0,比如当前时间是1,那么需要打印的字符串是001,如果是22,则是022,依次类推。

4.2 Commander类

承接Wow类里面调用Commander类的方法顺序,我们从initResource()开始分析:

void Commander::initResource( int m, const unsigned int *M ) {
    this->m = m;
    this->M = M;
    n = 0;
    std::fill( std::begin( C ), std::end( C ), 0 );
    idx = -1;
    is_stopped = false;
}

这个函数就是初始化/重置每个成员变量,记住不要忘记其他变量,因为程序运行时需要多次进行生产,比如战士计数就行进行重置为0。接下来是hasNext():

int Commander::hasNext() {
    // Have to explicitly cast M[ W_n[ ( idx + 1 ) % WARRIOR_NUM ] ] to int,
    // otherwise the result will always be unsigned int, which is never less than 0.
    if ( m - ( int ) M[ W_n[ ( idx + 1 ) % WARRIOR_NUM ] ] >= 0 )
        return ( idx + 1 ) % WARRIOR_NUM;
    else if ( m - ( int ) M[ W_n[ ( idx + 2 ) % WARRIOR_NUM ] ] >= 0 )
        return ( idx + 2 ) % WARRIOR_NUM;
    else if ( m - ( int ) M[ W_n[ ( idx + 3 ) % WARRIOR_NUM ] ] >= 0 )
        return ( idx + 3 ) % WARRIOR_NUM;
    else if ( m - ( int ) M[ W_n[ ( idx + 4 ) % WARRIOR_NUM ] ] >= 0 )
        return ( idx + 4 ) % WARRIOR_NUM;
    else if ( m - ( int ) M[ W_n[ ( idx + 5 ) % WARRIOR_NUM ] ] >= 0 )
        return ( idx + 5 ) % WARRIOR_NUM;

    return -1;
}

一眼望去,这个函数到底是干嘛的?这些if-else语句究竟在判断什么?我们先抛开if-else语句,先来看看这个函数的返回值。这个函数的返回值似乎总是( n % m ),这里n和m都是非负整数,那么我们知道一个非负整数模上另一个非负整数,那么结果范围一定是:[ 0, m ),再加上末尾返回的-1,那么这个函数返回值范围为:[ -1, m ),讲解到这里,大家对于这样的返回值能想到什么?什么东西能用到这样的返回值呢?没错哒,数组下标哒,也就是说如果返回非负整数范围,我们就需要到某个数组中取出对应的内容,如果返回-1,则表示无法从该数组中取出值。如果这个数组是战士的生产列表,那么该返回值就表示当前需要生产的战士下标,-1则表示没有生产能力。

那么接下来的问题就是,这个数组究竟是哪一个成员变量呢?不知道大家还记得我们之前提到了一个比较特殊的成员变量:

// 每个战士的名字列表
const int *W_n; // local warrior names

这个成员变量明明纪录的是名字,但类型为什么是int呢?这里我们把每个战士的名字映射到了一个数组下标当中,也就是枚举类Warrior里面定义的下标:

enum Warrior {
    dragon, // = 0
    ninja, // = 1
    iceman, // = 2
    lion, // = 3
    wolf // = 4
};

// 通过上面的Warrior枚举类,也就说我们我们定义了这样一个数据关系:
0         1        2         3       4
dragon    ninja    iceman    lion    wolf

那么我们可以通过这个下标的映射关系,将每个司令单独的战士名字,战士消耗的生命资源,映射到同一个下标下面。为了方便理解,我们举一个例子:

// 红司令生产战士顺序
0       1     2     3      4      // 正常下标
iceman  lion  wolf  ninja  dragon // 战士名字, 即WARRIOR_NAMES
1       2     3     4      5      // 生命资源消耗值,即M
// 将其映射到之前定义的枚举数组
0       1     2     3      4      // 正常下标
iceman  lion  wolf  ninja  dragon // 战士名字, 即WARRIOR_NAMES
1       2     3     4      5      // 生命资源消耗值,即M
2       3     4     1      0      // 枚举Warrior数组下标,即即W_n
// 假设当前hasnext()返回下标2,也就是说对于红司令的生产战士序列,它需要生产第三个战士,也就是wolf
// 那么我们可以通过W_n[ idx ] = 4获取它在枚举Warrior数据的全局下标,然后通过WARRIOR_NAMES和M分别获取该战士的名字和所消耗的生命资源
// 即WARRIOR_NAMES[ 4 ] = "wolf" 和 M[ 4 ]= 3
// 有童鞋可以有疑问,这样映射我们用正常下标就可以了,为什么要用枚举下标?
// 这是因为这里只有一个司令,如果有多个司令呢?我们知道所有司司令的WARRIOR_NAMES和M其实都是指向同一个数组,即:
0         1        2         3       4     // 枚举下标
dragon    ninja    iceman    lion    wolf  // 枚举成员变量/战士名字
5         4        1         2       3     // 生命资源消耗值
// 那么仅仅用上面的正常下标就不行了,除非每个司令都保存一个自己的WARRIOR_NAMES和M,但是这样空间开销就太大了

在理解了函数的返回值,接下来我们来分析一下if-else的判断条件代表什么?

// 解析:
m - ( int ) M[ W_n[ ( idx + k ) % WARRIOR_NUM ] ], where 1 <= k <= 5
// 首先,我们逐层剖析,从最里层开始:
( idx + k ) % WARRIOR_NUM
// 我们先解释一下的idx的意义,它表示上一轮司令已经生产的战士下标,比如红司令上一轮生产到wolf,那么idx = 2
// 那么为什么需要从1加到k呢?我们把目光暂时看到之前生产战士顺序的列表上面:
// 红司令生产战士顺序
0       1     2     3      4      // 正常下标
iceman  lion  wolf  ninja  dragon // 战士名字, 即WARRIOR_NAMES
               ^
              idx
// 那么如果我们分别加上1,2,3,有什么效果呢?
0       1     2        3           4      // 正常下标
iceman  lion  wolf     ninja       dragon // 战士名字, 即WARRIOR_NAMES
               ^         ^           ^
              idx     idx + 1      idx + 2
// 我们发现idx发生了右偏移,也就是说指向wolf后面可以生产的两个战士,即nijja和dragon
// 或者我们可以说当前我们需要从idx + 1开始生产,也就是从nijia开始生产
// 如果资源不够资源生产nijia,我们就看看能不能生产dragon
// 那么我们继续加上4,5呢?这不是下标溢出了么?
// 这里就需要 % WARRIOR_NUM ( = 5 ),也就是说让溢出的下标从0重新开始:
0            1            2        3           4      // 正常下标
iceman       lion         wolf     ninja       dragon // 战士名字, 即WARRIOR_NAMES
   ^           ^            ^        ^            ^
 idx_3       idx_4        idx_5    idx_1        idx_2
// where idx_5 = idx or ( idx + 5 ) % WARRIOR_NUM, idx_1 = ( idx + 1 ) % WARRIOR_NUM
// idx_2 = ( idx + 2 ) % WARRIOR_NUM, idx_3 = idx + 3 = ( idx + 3 ) % WARRIOR_NUM,
// idx + 4 = ( idx + 4 ) % WARRIOR_NUM

通过上面分析,我们知道了这个函数其实就是实现了下面这个功能:

如果司令部中的生命元不足以制造某个按顺序应该制造的武士,那么司令部就试图制造下一个。如果所有武士都不能制造了,则司令部停止制造武士。

当idx偏移了5次,还是不能生产,那么就返回-1,表示没有能力生产任何战士单位。那么if-eise里面的判断条件也非常好理解了:

( idx + 1 ) % WARRIOR_NUM // 表示当前需要生产的战士在当前司令生产列举中下标
W_n[ ( idx + 1 ) % WARRIOR_NUM ] // 表示这个战士在枚举下标,也就是全局下标
M[ W_n[ ( idx + 1 ) % WARRIOR_NUM ] ] // 表示这个战士需要消耗多少生命资源
m - M[ W_n[ ( idx + 1 ) % WARRIOR_NUM ] ] // 表示如果生产这个战士,还剩多少生命资源
// 也就是说上面的结果如果小于0,表示资源不够生产这个战士

这里需要注意一点,我们显性地转换了 M[ W_n[ ( idx + 1 ) % WARRIOR_NUM ] ] 到 int,否则最后结果会被转换成unsigned int,那么结果永远不可能为负数,也就是说司令有无限的生产能力,这不是游戏里面常见的bug嘛!如果hasnext()返回一个非负整数,那么我们就需要调用generate()函数进行生产了:

void Commander::generate( std::string &t ) {
    // 再次检查能否生产,并获取战士下标idx
    idx = hasNext();
    if ( idx < 0 ) {
        // 没有生产能力了,检查是否是第一次进入次代码
        if ( !is_stopped ) {
            // 如果是,则输出结束结果
            is_stopped = !is_stopped;
            printf(
                "%s %s headquarter stops making warriors\n",
                t.c_str(),
                COLOR_NAMES[ c ]
            );
        }

        return;
    }

    // 还有能力继续生产,获取战士相应的数据
    int idx_global = W_n[ idx ];
    unsigned int warrior_m = M[ idx_global ];
    const char *color_name = COLOR_NAMES[ c ];
    const char *warrior_name = WARRIOR_NAMES[ idx_global ].c_str();

    // 纪录消耗的生命资源
    m -= warrior_m;
    // 进行结果输出
    // https://stackoverflow.com/questions/10865957/how-to-use-printf-with-stdstring
    printf(
        OUTPUT_FORMAT,
        t.c_str(), // timestamp
        color_name,
        warrior_name,
        ++n, // Number
        warrior_m, // life/strength point
        ++C[ idx ], // Warrior count
        warrior_name,
        color_name
    );
}

这个函数其实就做了两件事:1)再次检查能否生产,并获取战士下标idx;2)并输出相应的结果。从上面代码也可以看出is_stopped的作用,它用来纪录是否是第一次结束生产,如果是则需要输出结束文本,从这里也可以看出我们为什么要在start()的while结束之后再次调用这个generate()函数,因为我们需要输出结束文本。到此,我们讲解完了本题的所有思路,以及程序设计思路,代码解析,有问题的童鞋可以留言或者发邮箱(见页面下方邮件图标)给我。

5. 项目代码

※ 以下习题答案全部通过OJ,使用编译器为:G++(9.3(with c++17))。

※ 因为本题是课程final大作业的前置作业,所以最终代码会针对后面题目的要求进行修改。之所以这么做是让大家能从一个大项目的角度来看待整个作业,而不是仅仅是一个作业,所以我们会着重于模块化,可读性,可维护性,因此那种把整个代码放在一个文件的情况在这个作业是不会出现的。但是为了应对作业提交的要求(提交只能是一个文件),这里都会提供一个可供提交的版本,大家可以对比提交版本和最终版本的区别,看看我们究竟是从一个小项目一步步创建一个大型项目的,这个能力在后期大家项目中也是非常有用的能力之一。

※ 由于篇幅所限,这里只会给到所有类、成员变量/函数的定义,具体实现请见github相关文件。

提交版本代码链接

此模块完整代码链接

Final大作业完整代码链接

// 这里只给到相关定义,完整代码请移步到github
enum Color {
    red,
    blue
};

enum Warrior {
    dragon, // = 0
    ninja, // = 1
    iceman, // = 2
    lion, // = 3
    wolf // = 4
};

/**
 * Class to generate warriors
 * */

class Commander {
    const static std::string WARRIOR_NAMES[ WARRIOR_NUM ]; // global warrior names
    const static char* OUTPUT_FORMAT;

    const Color c; // color
    int m; // total life points
    const int *W_n; // local warrior names
    const unsigned *M; // Warrior life points
    unsigned int n = 0; // warrior total count
    unsigned int C[ WARRIOR_NUM ] = { 0 }; // warrior count
    int idx = -1; // warrior index
    bool is_stopped = false;

public:
    Commander( Color c, const int *W ) : c( c ), W_n( W ) {}

    /**
     * Can this commander generate next warrior?
     *
     * @return An index to that warrior is returned if it can, otherwise -1 is returned.
     * */

    int hasNext();

    /**
     * Generate a warrior if this commander can do it.
     *
     * @param t Timestamp string in the format of "XXX"
     * */

    void generate( std::string &t );

    /**
     * Initialize resources.
     *
     * @param m Total life points.
     * @param M Array to indicate life points to generate a warrior.
     * */

    void initResource( int m, const unsigned *M );
};

/**
 * Class to coordinate the game.
 * */

class WorldOfWarcraft {
    const static int WARRIOR_NAMES_RED[ WARRIOR_NUM ];
    const static int WARRIOR_NAMES_BLUE[ WARRIOR_NUM ];

    unsigned int c = 0; // timestamp count
    Commander comm_red = Commander( red, WARRIOR_NAMES_RED ); // red headquarter
    Commander comm_blue = Commander( blue, WARRIOR_NAMES_BLUE ); // blue headquarter

    static std::string getTimeStr( unsigned int c );

public:
    /**
     * Start the game.
     *
     * @param n Case number.
     * @param m Total life points.
     * @param M Array to indicate life points to generate a warrior.
     * */

    void start( unsigned int n, unsigned int m, const unsigned int *M );
};

上一章:类和对象提高

下一章:运算符重载

6. 参考资料

  1. C++程序设计
  2. pixiv illustration: Arcaea CG 0-3

7. 免责声明

※ 本文之中如有错误和不准确的地方,欢迎大家指正哒~
※ 此项目仅用于学习交流,请不要用于任何形式的商用用途,谢谢呢;


在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值