Tango Tango Insurrection - UVA 10618

#dp #线性dp #模拟 #T4

Tango Tango Insurrection - UVA 10618 - Virtual Judge --- 探戈探戈起义 - UVA 10618 - 虚拟法官 (vjudge.net)

F 跳舞机

你想学着玩跳舞机。跳舞机的踏板上有4个箭头:上、下、下、右。当舞曲开始时,屏幕上会有一些箭头往上移动。当向上移动箭头与顶部的箭头模板重合时,你需要用脚踩一下踏板上的相同箭头。不需要踩箭头时,踩箭头并不会受到惩罚,但当需要踩箭头时,必须踩一下,哪怕已经有一只脚放在了该箭头上。很多舞曲的速度快,需要来回倒腾步子,所以最好写一个程序来帮助你选择一个轻松的踩踏方式,使得能量消耗最少。为了简单起见,将一个八分音符作为一个基本时间单位,每个时间单位要么需要踩一个箭头(不会同时需要踩两个箭头),要么什么都不需要踩。
在任意时刻,你的左右脚应放在 不同的两个箭头上,且每个时间单位内只有一只脚能动(移动和/或踩箭头),不能跳跃。另外,你必须面朝前方以看到屏幕(即:你不能把左脚放到右箭头上,并且右脚放到左箭头上)。 当你执行一个动作(移动和/或踩)时,消耗的能量这样计算:
■ 如果这只脚上个时间单位没有任何动作,消耗1单位能量。
■ 如果这只脚上个时间单位没有移动,消耗3单位能量。
■ 如果这只脚上个时间单位移动到相邻箭头,消耗5单位能量。
■ 如果这只脚上个时间单位移动到相对箭头(上到下,或者左到右),消耗7单位能量。

正常情况下,你的左脚不能放到右箭头上(或者反之),但有一种情况例外:如果你的左脚在上箭头或者下箭头,你可以临时扭着身子用右脚踩左箭头,但是在你的右脚移出左箭头之前,你的左脚都不能移到另一个箭头上。类似地,右脚在上箭头或者下箭头时,你也可以临时用左脚踩右箭头。一开始,你的左脚在左箭头上,右脚在右箭头上。

输入

多组数据(不超过100组),每组数据包含一行——一个由'L','R','D','U'以及'.'组成的字符串(长度不超过70),第i个字符表示时间为i时需要踩下的键。

输出

对于每组数据输出一个串,第i个字符表示时间为i时移动哪只脚,不移动输出'.'。

样例输入

LRLRLLLLRLRLRRRRLLRRLRLDU...D...UUUUDDDD  
#

样例输出

LRLRLLLLRLRLRRRRLLRRLRLRL...R...LLLLRRRR

思路

代码直接照搬紫书, 没找到比它更简洁的(

给一个只包含上下左右箭头的跳舞机, 和一串只包含LRDU.代表左右下上的字符串, 你有两只脚, 起初左脚放在左箭头, 右脚放在右箭头, 当新的箭头来临时, 需要选择一只脚踩对应的箭头。
每只脚有4种操作, 其消耗的能量不同:

  • 不动, 1能量
  • 原位置踩一下, 3能量
  • 移动到相邻位置并踩一下, 5能量, 如←移动到↑↓。
  • 移动到相对位置并踩一下, 7能量, 如↑移动到↓。
    求出能量小号最小的移动方案, 能够在某个时刻的箭头来临时踩对应的箭头位置, 每个时刻只输出动了哪只脚。

题目只需要求出左右脚哪只脚动的方案, 但我们不能只以结果来处理, 每个时刻需要满足踩到四个位置之一, 故选择的数据结构需要能描述当前时刻哪只脚踩到了哪个位置。

那么一个3维数组 \(f[i][a][b]\) 就可以代表 i 时刻, 左脚在 a 位置, 右脚在 b 位置。但对于 \(f(i,a,b)\) 状态我们还需要知道从\(i-1\)秒动了哪个脚走到\(i\)的, 否则不满足不重不漏的要求。那么就再加一维s, 取值为012, 代表没动、动左脚、动右脚。

找到合适的数据结构之后就可以考虑如何得出最小能量消耗, 其中a和b的取值为0123分别代表上左右下。

先考虑DFS搜索, 时间复杂度很难满足, 假设每种方案有2个选择, 最后也是 \(2^{70}\), 必超时。因为时间的流逝是不可逆的, 也就是从 \(i\) 时刻不可能回到 \(i-1\) 时刻, 即无自环, 那么就满足DP的使用条件。

对于状态 \(f(i,a,b,s)\) 我们直接四维循环枚举前一个状态, 这样的复杂度总共也就 \(70\times 4 \times 4 \times 4= 4480\)。为了提高代码可读性, 减少调试的复杂度, 我们可以像定义 EOF = -1 一样, 把四个箭头用 (UP, LEFT, RIGHT, DOWN) = (0,1,2,3,4) 来代替。

像这种状态定义简单但转移复杂的题目就像个复杂的模拟题一样, 做题策略也是逐步尽可能的降低复杂度, 才会无痛AC。

题目给的是LRUD的字符串, 咱们也给他用一个数组映射到数值, 即 \(pos['U']=UP\)。为了方便之后输出, 我们开个数组 action 记录当前状态的前继状态, 要用 \((i,a_i,b_i,s_i)\rightarrow (j,a_j,b_j,s_j)\) 来映射嘛?紫书给了更优雅的做法, 既然 \(j=i-1\) , 那么知道了当前 \(state_i\) 转移过来所用的脚 \(f_i\), 和对应脚移动前的位置 \(t_i\), 就可以得出前一个状态为 \((i-1,t_i,b_i,f_i)\) , 可以直接从 \(state_i\) 推得。

而且也不用定义pair来存 \(f_i,t_i\) , 既然 \(f\)\(t\) 的取值范围只有0~4, 那么直接用数值哈希表 \(hash_i = f_i\times 4 + t_i\) , 唯一地记录ft, 且可以用 \(\frac{hash_{i}}{4}=f, hash_{i}\mod 4 = t\) 来直接得到对应的 \(f,t\)

至此, 就可以用 \(action[i][a][b][s] = hash_i\) 来记录 \((i,a_i,b_i,s_i)\rightarrow (j,a_j,b_j,s_j)\) 的转移路径信息。状态转移的话就从当前状态 \(state_i\), 根据对应的时刻应该踩的位置, 枚举前一个状态动左脚和右脚的情况, 如果当前时刻不用踩, 也需要枚举前一个状态左脚右脚移动到任意位置的时候。

实现细节请看代码及注释, 紫书上该题代码的优雅程度是可以打印下来抱着睡觉的那种(

代码

代码链接:
代码链接 | Try It Online

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

const int UP = 0;
const int LEFT = 1;
const int RIGHT = 2;
const int DOWN = 3;

const int N = 70 + 5;
int d[N][4][4][3];
// dp数组, 到第 i 个箭头, 当前左脚位置a, 当前右脚位置b, 移动到该位置时是1左脚移动还是2右脚移动还是0没移动s

int action[N][4][4][3];

char seq[N], pos[256], footch[] = ".LR";

// 从 a 位置移动到 ta 位置并踩一下所需的能量
int energy(int a, int ta)
{
    if (a == ta) return 3; // 位置不变, 只是踩一下
    if (a + ta == 3) return 7; // 移动对角的话比如 UP移动到 DOWN, 和LEFT移动到RIGHT, 可以发现两种方式相加都是3, 故可以依次来判断
    return 5;
}

// 当前状态f(i,a,b,s), 从前一个位置转移过来要移动的脚f(0都不动, 1左脚,2右脚), 要移动的位置t, 上一个状态的左脚位置ta, 右脚位置tb
int energy(int i, int a, int b, int s, int f, int t, int &ta, int &tb)
{
    ta = a, tb = b;
    if (f == 1) ta = t;
    else if (f == 2) tb = t;

    // 判断移动后的位置是否成立
    if (ta == tb) return -1;
    if (ta == RIGHT && tb == LEFT) return -1;
    if (a == RIGHT && tb != b) return -1; // 特殊状态, 当右脚在UP或DOWN的时候左脚可以在RIGHT位置, 但右脚无法移动
    if (b == LEFT && ta != a) return -1;

    // 计算移动所需要消耗的能量
    int e;
    if (f == 0)
        e = 0;
    else if (f != s)
        e = 1; // 若脚不是需要移动的脚, 则说明是不动的脚
    else
    {
        if (f == 1)e = energy(a, ta);
        else e = energy(b, tb);
    }
    return e;
}

// 将前一个状态的 f 脚移动到 t 位置, 以转移到 当前状态f(i,a,b,s) 
void update(int i, int a, int b, int s, int f, int t)
{
    int ta, tb;
    int e = energy(i, a, b, s, f, t, ta, tb);
    if (e < 0)
        return;

    int cost = d[i + 1][ta][tb][f] + e;
    int &ans = d[i][a][b][s];
    if (cost < ans)
    {
        ans = cost;
        action[i][a][b][s] = f * 4 + t;
    }
}

void solve()
{
    pos['U'] = UP, pos['L'] = LEFT, pos['R'] = RIGHT, pos['D'] = DOWN;

    int n = strlen(seq);
    memset(d, 0, sizeof d);

    for (int i = n - 1; i >= 0; i--)
        for (int a = 0; a < 4; a++)
            for (int b = 0; b < 4; b++) if (a != b)
                    for (int s = 0; s < 3; s++)
                    {
                        d[i][a][b][s] = 10 * n; // 随意定的最大值
                        if (seq[i] == '.')
                        {
                            update(i, a, b, s, 0, 0); // 可以俩脚都不动
                            for (int t = 0; t < 4; t++)
                            {
                                update(i, a, b, s, 1, t); // 将左脚移动到t位置
                                update(i, a, b, s, 2, t); // 将右脚移动到t位置
                            }
                        }
                        else
                        {
                            update(i, a, b, s, 1, pos[seq[i]]); // 移动左脚到需要的位置
                            update(i, a, b, s, 2, pos[seq[i]]); // 移动右脚到需要的位置
                        }
                    }

    // 输出答案
    int a = LEFT, b = RIGHT, s = 0; // 终止状态
    for (int i = 0; i < n; i++)
    {
        int f = action[i][a][b][s] / 4;
        int t = action[i][a][b][s] % 4;
        cout << footch[f];
        s = f;
        if (f == 1) a = t;
        else if (f == 2) b = t;
    }
    cout << endl;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    while (cin >> seq)
    {
        if (seq[0] == '#')
            break;
        solve();
    }
    return 0;
}
posted @ 2023-05-11 20:58  EdwinAze  阅读(34)  评论(0编辑  收藏  举报