[传智杯 #5 初赛] F-二人的大富翁游戏

GitHub跳转原题关系图返回列表

按操作顺序直接模拟移动、过路结算、建造升级和轮末收益,维护建筑拥有者、等级与价值即可。

OJ: luogu

题目 ID: P8874

难度:普及/提高-

标签:模拟思维

日期: 2026-06-19 01:38

题意

有两个玩家在一个环形地图上移动。

每次行动会:

  1. 按骰子点数走若干步;
  2. 路过自己的建筑就收钱,路过对方的建筑就付钱;
  3. 停下后,可能在当前位置建造或升级自己的建筑;
  4. 每轮两个人都行动完后,所有建筑还会再给拥有者一笔固定收益。

如果某个人在自己行动过程中资金变成负数,这个人立刻输掉游戏。

给出整个操作序列,问谁会输;如果一直没人输,就输出最后两人的资金。

思路

先看一个可以直接验证想法的朴素解:

严格按题意把整场游戏一步一步模拟出来,维护:

  • 两人的当前位置
  • 两人的资金
  • 每个格子是否有建筑
  • 建筑属于谁、等级是多少、当前价值 a_i 是多少
cpp
// brute.cpp:按题目规则直接模拟整场游戏。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 105;
const int MAXL = 105;
const int RENKO = 0;
const int MERRY = 1;

struct Op {
    int type;
    int val;
};

int n, q, L;
long long m;
int c[MAXN][MAXL];
int d[MAXN];
vector<Op> ops;

int pos_[2];
long long money[2];
int owner[MAXN];
int level_[MAXN];
long long value_[MAXN];

void give_round_income() {
    for (int i = 1; i <= n; i++) {
        if (owner[i] != -1) {
            money[owner[i]] += d[i];
        }
    }
}

bool move_player(int who, int step) {
    int other = who ^ 1;

    for (int i = 1; i <= step; i++) {
        pos_[who]++;
        if (pos_[who] > n) {
            pos_[who] = 1;
        }

        int p = pos_[who];
        if (owner[p] == who) {
            money[who] += value_[p];
        }
        else if (owner[p] == other) {
            money[who] -= value_[p];
            money[other] += value_[p];
            if (money[who] < 0) {
                return false;
            }
        }
    }

    return true;
}

void build_or_upgrade(int who, int times) {
    int p = pos_[who];

    if (owner[p] != -1 && owner[p] != who) {
        return;
    }

    if (owner[p] == -1) {
        if (times >= 1 && money[who] >= c[p][0]) {
            money[who] -= c[p][0];
            owner[p] = who;
            level_[p] = 1;
            value_[p] = c[p][0];
            times--;
        }
        else {
            return;
        }
    }

    for (int t = 1; t <= times; t++) {
        if (level_[p] >= L) {
            break;
        }
        if (money[who] < c[p][level_[p]]) {
            break;
        }
        money[who] -= c[p][level_[p]];
        value_[p] += c[p][level_[p]];
        level_[p]++;
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m >> q >> L;
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= L - 1; j++) {
            cin >> c[i][j];
        }
    }
    for (int i = 1; i <= n; i++) {
        cin >> d[i];
    }

    while (true) {
        Op op;
        if (!(cin >> op.type >> op.val)) {
            break;
        }
        ops.push_back(op);
    }

    for (int i = 1; i <= n; i++) {
        owner[i] = -1;
    }
    pos_[RENKO] = 1;
    pos_[MERRY] = 1;
    money[RENKO] = m;
    money[MERRY] = m;

    int move_cnt = 0;
    for (int i = 0; i < (int)ops.size(); i++) {
        if (ops[i].type != 1) {
            continue;
        }

        int who = move_cnt % 2;
        if (!move_player(who, ops[i].val)) {
            if (who == RENKO) {
                cout << "Renko\n";
            }
            else {
                cout << "Merry\n";
            }
            return 0;
        }

        if (i + 1 < (int)ops.size() && ops[i + 1].type == 2) {
            build_or_upgrade(who, ops[i + 1].val);
            i++;
        }

        move_cnt++;
        if (move_cnt % 2 == 0) {
            give_round_income();
        }
    }

    cout << money[RENKO] << ' ' << money[MERRY] << '\n';

    return 0;
}

这题本质上没有隐藏算法,重点是把规则实现完整、顺序实现正确。

真正容易出错的地方主要有四个:

  1. 1 k 操作才会切换行动者,2 k 不会切换;
  2. 每个 1 k 后面至多跟一个 2 k,所以要按顺序读操作;
  3. 路过格子时,终点格子也算“经过”;
  4. 每两次 1 k 操作结束后,才统一结算一轮建筑收益。

因此正式做法依然就是模拟,只是把几个动作拆清楚:

  1. 处理 1 k:逐步移动,并在经过每个格子时结算建筑效果;
  2. 如果后面紧跟 2 k:尝试建造/升级;
  3. 每完成两次 1 k:遍历所有建筑,给拥有者发一轮固定收益;
  4. 过程中一旦当前行动者资金变成负数,就立即输出输家。

因为题目规模只有 n,L <= 100q <= 10^4,直接模拟完全足够。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 105;
const int MAXL = 105;
const int RENKO = 0;
const int MERRY = 1;

struct Op {
    int type;
    int val;
};

int n, q, L;
long long m;
int c[MAXN][MAXL];
int d[MAXN];
vector<Op> ops;

int pos_[2];
long long money[2];
int owner[MAXN];         // -1 表示无建筑,0 表示莲子,1 表示梅莉
int level_[MAXN];        // 建筑当前等级
long long value_[MAXN];  // 当前建筑的 a_i

void give_round_income() {
    for (int i = 1; i <= n; i++) {
        if (owner[i] != -1) {
            money[owner[i]] += d[i];
        }
    }
}

bool move_player(int who, int step) {
    int other = who ^ 1;

    for (int i = 1; i <= step; i++) {
        pos_[who]++;
        if (pos_[who] > n) {
            pos_[who] = 1;
        }

        int p = pos_[who];
        if (owner[p] == who) {
            money[who] += value_[p];
        }
        else if (owner[p] == other) {
            money[who] -= value_[p];
            money[other] += value_[p];
            if (money[who] < 0) {
                return false;
            }
        }
    }

    return true;
}

void build_or_upgrade(int who, int times) {
    int p = pos_[who];

    if (owner[p] != -1 && owner[p] != who) {
        return;
    }

    if (owner[p] == -1) {
        if (times >= 1 && money[who] >= c[p][0]) {
            money[who] -= c[p][0];
            owner[p] = who;
            level_[p] = 1;
            value_[p] = c[p][0];
            times--;
        }
        else {
            return;
        }
    }

    while (times > 0) {
        if (level_[p] >= L) {
            break;
        }
        if (money[who] < c[p][level_[p]]) {
            break;
        }
        money[who] -= c[p][level_[p]];
        value_[p] += c[p][level_[p]];
        level_[p]++;
        times--;
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m >> q >> L;
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= L - 1; j++) {
            cin >> c[i][j];
        }
    }
    for (int i = 1; i <= n; i++) {
        cin >> d[i];
    }

    while (true) {
        Op op;
        if (!(cin >> op.type >> op.val)) {
            break;
        }
        ops.push_back(op);
    }

    for (int i = 1; i <= n; i++) {
        owner[i] = -1;
    }
    pos_[RENKO] = 1;
    pos_[MERRY] = 1;
    money[RENKO] = m;
    money[MERRY] = m;

    int move_cnt = 0;
    for (int i = 0; i < (int)ops.size(); i++) {
        if (ops[i].type != 1) {
            continue;
        }

        int who = move_cnt % 2;
        if (!move_player(who, ops[i].val)) {
            if (who == RENKO) {
                cout << "Renko\n";
            }
            else {
                cout << "Merry\n";
            }
            return 0;
        }

        if (i + 1 < (int)ops.size() && ops[i + 1].type == 2) {
            build_or_upgrade(who, ops[i + 1].val);
            i++;
        }

        move_cnt++;
        if (move_cnt % 2 == 0) {
            give_round_income();
        }
    }

    cout << money[RENKO] << ' ' << money[MERRY] << '\n';

    return 0;
}

复杂度

设一共读到 2q 次移动操作,总移动步数之和为 S

时间复杂度是 O(S+nq)O(S + nq),空间复杂度是 O(nL)O(nL)

总结

这题的关键不是优化,而是规则实现的顺序必须绝对准确。

只要把“移动结算、建造升级、轮末收益、输家判定”这几个时机分开写清楚,代码就会很稳。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析