牌堆

牌堆

n 张纸牌,编号 1n

其中,第 i 张纸牌的价值为 vi

你要玩一个游戏,具体流程如下:

首先,你要将 n 张牌一张叠一张地堆成一个牌堆,牌堆中纸牌的具体排列顺序由你决定。

接下来,你需要依次进行 m 次操作。

关于第 i 次操作:

  • 你的任务是将编号ai 的纸牌从牌堆中抽出,并置于牌堆顶部。
  • 如果执行此次操作时,不存在纸牌位于纸牌 ai 之上,即纸牌 ai 本来就位于牌堆顶部,则此次操作无需付出任何代价。
  • 如果执行此次操作时,存在纸牌位于纸牌 ai 之上,则此次操作需要付出代价,具体代价为所有位于纸牌 ai 之上的纸牌的价值之和(不要将价值和编号混淆)。

请你合理安排初始时牌堆中的纸牌排列顺序,从而使得执行完所有操作需要付出的总代价尽可能小。

输出总代价的最小可能值。

例如,如果一共有 3 张纸牌,价值分别为 1,2,3,一共需要进行 5 次操作,每次操作需要置于牌堆顶部的卡牌分别为 1,3,2,3,1

那么一种最佳的初始时牌堆中纸牌排列顺序为从上到下依次为纸牌 1,3,2

具体操作过程如下:

  • 1 次操作,需要将纸牌 1 置于牌堆顶部,由于纸牌 1 本来就在牌堆顶部,因此,所需代价为 0,执行完此次操作后,牌堆中纸牌排列顺序仍为 1,3,2
  • 2 次操作,需要将纸牌 3 置于牌堆顶部,由于纸牌 1 位于纸牌 3 之上,因此,所需代价为 v1=1,执行完此次操作后,牌堆中纸牌排列顺序为 3,1,2
  • 3 次操作,需要将纸牌 2 置于牌堆顶部,由于纸牌 1,3 位于纸牌 2 之上,因此,所需代价为 v1+v3=4,执行完此次操作后,牌堆中纸牌排列顺序为 2,3,1
  • 4 次操作,需要将纸牌 3 置于牌堆顶部,由于纸牌 2 位于纸牌 3 之上,因此,所需代价为 v2=2,执行完此次操作后,牌堆中纸牌排列顺序为 3,2,1
  • 5 次操作,需要将纸牌 1 置于牌堆顶部,由于纸牌 2,3 位于纸牌 1 之上,因此,所需代价为 v2+v3=5,执行完此次操作后,牌堆中纸牌排列顺序为 1,3,2

付出的总代价为 0+1+4+2+5=12

输入格式

第一行包含两个整数 n,m

第二行包含 n 个整数 v1,v2,,vn

第三行包含 m 个整数 a1,a2,,am

输出格式

一个整数,表示总代价的最小可能值。

数据范围

3 个测试点满足 2n31m5
所有测试点满足 2n5001m10001vi1001ain

输入样例:

3 5
1 2 3
1 3 2 3 1

输出样例:

12

 

解题思路

  猜对了做法,来补一下证明。

  先看第1次操作a1,考虑a1在初始卡牌中的位置。如果直接把a1放在开头那么第1次操作就不需要执行,代价为0。否则把a1放在后面的位置(除开头外的位置),那么代价就是前面所有牌的价值之和(大于0)。因此我们可以把a1在初始卡牌中的位置分成两类,一类是放开头,另一类是不放开头。可以发现对于不放开头的情况,在操作完后a1就变到了的开头的位置,这就变成了a1放开头的情况。另外可以发现,不管是a1放开头还是不放开头,在操作后其他牌的顺序都不变,即得到的结果都一样,而不放开头则会产生代价,因此必然选择第一种方案(a1放开头)最优。

  接着考虑a2,如果a2=a1,由于本身就在开头因此代价为0。否则a2a1,一样的可以把a2在初始卡牌中的位置分成两类:一类是放第2个位置(a1已经放在第1个位置了),另一类是放第2个位置之后的位置。用同样的分析方法可以发现选择第一种方案最优。

  更一般的,定义p表示初始卡牌在经过每次操作后变化的结果。考虑ai,如果ai没有在之前出现过,那么ai应该在初始卡牌中的第k个位置,其中k表示1i1中出现过的牌的种类(因为每次操作后都要把牌移到开头,因此之前出现过的牌必然在ai的前面)。要把ai移动到开头,代价就是j=1kvpj。如果ai在之前出现过,那么直接在p中找到ai的位置j,累加前面的牌的价值就好了,代价就是u=1j1vpu,然后再把ai移到开头。

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

复制代码
 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 const int N = 510, M = 1010;
 5 
 6 int v[N], a[M], p[N];
 7 bool vis[N];
 8 
 9 int main() {
10     int n, m;
11     scanf("%d %d", &n, &m);
12     for (int i = 1; i <= n; i++) {
13         scanf("%d", v + i);
14     }
15     for (int i = 1; i <= m; i++) {
16         scanf("%d", a + i);
17     }
18     int ret = 0;
19     for (int i = 1, k = 0; i <= m; i++) {
20         if (!vis[a[i]]) {   // a[i]在之前没有出现过
21             vis[a[i]] = true;
22             for (int j = k; j; j--) {   // 1~k都往后移一位,把a[i]放开头,同时累加前面牌的代价
23                 p[j + 1] = p[j];
24                 ret += v[p[j]];
25             }
26             p[1] = a[i];
27             k++;    // 多了一种牌
28         }
29         else {  // a[i]在之前出现过
30             for (int j = 1; j <= k; j++) {
31                 if (p[j] == a[i]) { // 找到a[i]的位置
32                     for (int u = j - 1; u; u--) {   // 1~j都往后移一位,把a[i]放开头,同时累加前面牌的代价
33                         p[u + 1] = p[u];
34                         ret += v[p[u]];
35                     }
36                     p[1] = a[i];
37                     break;
38                 }
39             }
40         }
41     }
42     printf("%d", ret);
43     
44     return 0;
45 }
复制代码

 

参考资料

  AcWing 4955. 牌堆(AcWing杯 - 周赛):https://www.acwing.com/video/4702/

posted @   onlyblues  阅读(40)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
Web Analytics
点击右上角即可分享
微信分享提示