Tokitsukaze and Slash Draw

Tokitsukaze and Slash Draw

题目描述

在游戏王中有一张魔法卡叫做「一击必杀!居合抽卡」。

简单来说,当你发动这张卡后,你会从卡组最上方抽取一张卡,如果那张卡是「一击必杀!居合抽卡」的情况下,有大概率''一击必杀''打败对手。通常这张卡被玩家们称作''拔刀''。

在游戏王中,许多卡拥有着能够调整卡组中卡片顺序的效果,例如:「魔救」系列。

这个系列的卡大多包含''从自己卡组上面把5张卡翻开,可以...,剩下的卡用喜欢的顺序回到卡组最下面''的效果。也就是说,可以通过「魔救」系列的卡片效果,来调整「一击必杀!居合抽卡」在卡组中的位置。

--- 虽然那天我赢了很多局,但是当他拔刀的那一刻我才发现,他赢我太多了。

''哇哦,用魔救拔刀也太帅了吧'',Tokitsukaze 心里想道。她关闭知乎,打开《Yu-Gi-Oh Master Duel》准备学习如何操作,现在她正在人机练习中。

Tokitsukaze 的卡组有 n 张卡片,她已经知道「一击必杀!居合抽卡」这张卡片是在卡组中从最下面往上数的第 k 张卡片。她挑选了 m 种能够调整卡组中卡片顺序的卡片,第 i 种类型的卡片效果是:把卡组最上方的 ai 张卡片拿出来按顺序放到卡组最下方。这个效果你可以理解为:首先将一张卡片拿出来,然后把这张卡片放到卡组最下方,这个动作重复做 ai 次。而发动第 i 种类型的卡片效果,需要支付 bi 的''cost''。

Tokitsukaze 可以通过一些操作使每种类型的卡片可以发动无限次,她是否能够让「一击必杀!居合抽卡」这张卡片出现在卡组的最上方?如果能,请输出最少需要支付的''cost'',如果不能,请输出 ''-1''(不带引号)。

输入描述:

第一行包含一个整数 T (1T1000),表示 T 组测试数据。

对于每组测试数据:

第一行包含三个整数 n, m, k (1n5000; 1m1000; 1kn),表示 Tokitsukaze 的卡组有 n 张卡片,她挑选了 m 种能够调整卡组中卡片顺序的卡片,以及「一击必杀!居合抽卡」这张卡片是在卡组中从最下面往上数的第 k 张卡片。

接下来 m 行,每行两个整数 ai​, bi​ (1ain, 1bi109),表示第 i 种类型的卡片能将卡组最上方的 ai​ 张卡片拿出来按顺序放到卡组最下方,并且需要支付 bi​ 的"cost"才能发动。

保证 n 不超过 5000m 不超过 1000

输出描述:

对于每组测试数据,如果 Tokitsukaze 能够让「一击必杀!居合抽卡」这张卡片出现在卡组的最上方,输出一个整数表示最少需要支付的"cost";否则输出 "-1"(不带引号)。

示例1

输入

2
10 1 7
2 100
10 3 7
2 3
6 1
5 10

输出

-1
13

说明

第二组测试数据:

最开始「一击必杀!居合抽卡」这张卡片的位置在从下往上数的第 7 张,操作的方式可以是:

使用第 3 种类型的卡片,将位置从 7 移动到 2,移动的过程是 7891012

使用第 2 种类型的卡片,将位置从 2 移动到 8,移动的过程是 2345678

使用第 2 种类型的卡片,将位置从 8 移动到 4,移动的过程是 89101234

使用第 2 种类型的卡片,将位置从 4 移动到 10,移动的过程是 45678910

所以总共使用了 1 张第 3 种类型的卡片以及 3 张第 2 种类型的卡片,总支付的"cost"为 110+31=13

可以发现无法支付比 13 更少的"cost"使「一击必杀!居合抽卡」这张卡片出现在卡组的最上方,所以答案为 13

 

解题思路

  我赛时的做法与出题人给出的两种做法都不同()

  首先对于任意一种操作,本质上是将目标卡片循环右移 ai 次,所以对于某种操作方案,操作的顺序并不会影响目标卡片最终的位置。假设第 i 种操作执行了 ci 次,那么为了使得目标卡片从一开始的第 k 个位置移动到第 n 个位置,执行的操作应该要满足:

(k1+i=1mciai)modn=n1i=1mciaink(modn)

  所以就可以 dp,看每个 ci 选多少使得总移动步数在模 n 意义下为 nk 时总代价最小。定义 f(i,j) 表示执行前 i 个操作使得移动步数模 nj 的所有方案的最小代价。根据第 i 个操作执行的次数进行状态划分,容易知道每个操作实际上最多会执行 n1 次,因此如果直接转移的话时间复杂度就是 O(mn2)。然后发现这个问题与完全背包很像,考虑能不能用完全背包的方式进行优化。

  其中 f(i,j)f(i,(jai)modn) 的状态转移方程分别为:

f(i,j)=min{f(i1,j),f(i1,(jai)modn)+bi,f(i1,(j2ai)modn)+2bi,}f(i,(jai)modn)=min{f(i1,(jai)modn),f(i1,(j2ai)modn)+bi,f(i1,(j3ai)modn)+2bi,}

  因此就有 f(i,j)=min{f(i1,j),f(i,(jai)modn)+bi}。但又发现这个转移是存在环的,不过由于边权都是是非负的,因此可以对这个状态转移图跑 Dijkstra。对于当前状态 f(i,j),可以转移到的下一个状态有 f(i+1,j)f(i,(j+ai)modn),因此每个状态都有两条出边。

  最终的答案就是 f(m,nk)

  AC 代码如下,时间复杂度为 O(mnlogmn)

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

typedef long long LL;

const int N = 5010, M = 1010;
const LL INF = 0x3f3f3f3f3f3f3f3f;

int a[M], b[M];
LL dist[M][N];
bool vis[M][N];

void solve() {
    int n, m, k;
    scanf("%d %d %d", &n, &m, &k);
    for (int i = 1; i <= m; i++) {
        scanf("%d %d", a + i, b + i);
    }
    for (int i = 0; i <= m; i++) {
        for (int j = 0; j < n; j++) {
            dist[i][j] = INF;
            vis[i][j] = false;
        }
    }
    dist[0][0] = 0;
    priority_queue<array<LL, 3>> pq;
    pq.push({0, 0, 0});
    while (!pq.empty()) {
        auto p = pq.top();
        pq.pop();
        if (vis[p[1]][p[2]]) continue;
        vis[p[1]][p[2]] = true;
        if (p[1] == m && p[2] == n - k) break;    // 这个剪枝一定要加,不然会超时,很奇怪
        if (p[1] + 1 <= m && dist[p[1] + 1][p[2]] > -p[0]) {    // 第一维不能超过m,
            dist[p[1] + 1][p[2]] = -p[0];
            pq.push({p[0], p[1] + 1, p[2]});
        }
        if (dist[p[1]][(p[2] + a[p[1]]) % n] > -p[0] + b[p[1]]) {
            dist[p[1]][(p[2] + a[p[1]]) % n] = -p[0] + b[p[1]];
            pq.push({p[0] - b[p[1]], p[1], (p[2] + a[p[1]]) % n});
        }
    }
    printf("%lld\n", dist[m][n - k] == INF ? -1 : dist[m][n - k]);
}

int main() {
    int t;
    scanf("%d", &t);
    while (t--) {
        solve();
    }

    return 0;
}

  上面有提到每个操作实际上最多只会执行 n1 次,因此可以转换成多重背包问题。通过最简单的二进制优化就可以将时间复杂度降到 O(mnlogn),当然还可以用单调队列来优化,这样就会进一步降到 O(mn),不过单调队列的写法早忘了,这里就偷懒用二进制优化 qwq。

  AC 代码如下,时间复杂度为 O(mnlogn)

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

typedef long long LL;

const int N = 5010, M = 1010;

int a[M], b[M];
LL f[2][N];

void solve() {
    int n, m, k;
    scanf("%d %d %d", &n, &m, &k);
    for (int i = 1; i <= m; i++) {
        scanf("%d %d", a + i, b + i);
    }
    memset(f[0], 0x3f, n + 10 << 3);
    f[0][0] = 0;
    int s = 0;
    for (int i = 1; i <= m; i++) {
        int c = n;
        for (int k = 1; k <= c; k <<= 1) {
            memset(f[++s & 1], 0x3f, n + 10 << 3);
            for (int j = 0; j < n; j++) {
                f[s & 1][j] = min(f[s - 1 & 1][j], f[s - 1 & 1][((j - k * a[i]) % n + n) % n] + 1ll * k * b[i]);
            }
            c -= k;
        }
        if (c) {
            memset(f[++s & 1], 0x3f, n + 10 << 3);
            for (int j = 0; j < n; j++) {
                f[s & 1][j] = min(f[s - 1 & 1][j], f[s - 1 & 1][((j - c * a[i]) % n + n) % n] + 1ll * c * b[i]);
            }
        }
    }
    printf("%lld\n", f[s & 1][n - k] == 0x3f3f3f3f3f3f3f3f ? -1 : f[s & 1][n - k]);
}

int main() {
    int t;
    scanf("%d", &t);
    while (t--) {
        solve();
    }
    
    return 0;
}

  再给出出题人提到的建图跑最短路的做法,出题人给出的 dp 做法我看不懂 qwq。

  为了方便这里将下标 1n 映射为 0n1,问题可以抽象成在模 n 意义下起点为 k1 终点为 n1 的最短路问题。当在第 x 个位置时,此时可以通过执行第 i 种操作从 x 移动到 (x+ai)modn。因此对于每个下标 x[0,n1],枚举 m 个操作进行建图,令 x(x+ai)modn 的边权为 bi。然后对这个图跑 Dijkstra 即可。我这里跑的是朴素 Dijkstra 算法。

  AC 代码如下,时间复杂度为 O(nm+n2)

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

typedef long long LL;

const int N = 5010, M = 1010;
const LL INF = 0x3f3f3f3f3f3f3f3f;

int a[M], b[M];
LL g[N][N];
LL dist[N];
bool vis[N];

void solve() {
    int n, m, k;
    scanf("%d %d %d", &n, &m, &k);
    for (int i = 0; i < m; i++) {
        scanf("%d %d", a + i, b + i);
    }
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            g[i][j] = INF;
        }
        for (int j = 0; j < m; j++) {
            int t = (i + a[j]) % n;
            g[i][t] = min<LL>(g[i][t], b[j]);
        }
    }
    memset(dist, 0x3f, n + 10 << 3);
    dist[k - 1] = 0;
    memset(vis, 0, n + 10);
    for (int i = 0; i < n; i++) {
        int t = -1;
        for (int i = 0; i < n; i++) {
            if (!vis[i] && (t == -1 || dist[i] < dist[t])) t = i;
        }
        vis[t] = true;
        for (int i = 0; i < n; i++) {
            dist[i] = min(dist[i], dist[t] + g[t][i]);
        }
    }
    printf("%lld\n", dist[n - 1] == INF ? -1 : dist[n - 1]);
}

int main() {
    int t;
    scanf("%d", &t);
    while (t--) {
        solve();
    }
    
    return 0;
}

 

参考资料

  【题解】2024牛客寒假算法基础集训营2:https://ac.nowcoder.com/discuss/1251379/

posted @   onlyblues  阅读(133)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
历史上的今天:
2022-02-06 金发姑娘和N头牛
Web Analytics
点击右上角即可分享
微信分享提示