【题解】CodeForces:Omkar and Bed Wars


【前情紧要】

小熊同学比较菜,刚刚入门动态规划不久,也没有CodeForces账号,但是,我使用了别的题解给出的答案,自己验证了几个答案,都是一样的,如果提交上去有几个点没过,可能是一些小细节没考虑到,在评论区DD我就好啦。

写的比较详细。主要是为了让初学者也能看懂QWQ。所以,给我一个攒攒耶!
本题解法多种多样,文后给出其他博客的解答方法。


【题目描述】

原题地址:

COdeForces:Omkar and Bed Wars

题目大意:

n个人站成一圈,对于每一个人,他可以指向右边的人,他也可以指向左边的人。我们用L,R来表示。
现在给出一个规则:如果一个人(第i个人),没有被左右两边的人(第i-1或i+1),所指,那么他(第i个人)可以随便志向左边的人或右边的人。如果一个人,被左右两边中其中一个人所指,那么他必须要指向,左右两边其中指向他的人。如果一个人同时被左右两边的人所指,他随便指向左右两边即可。
问,对于一个n的xxxxx(x为L或者R,x总共n个,代表这i个人的指向关系)这样的指向关系,我们最少修改几次L变成R,或R变成L,使得它符合游戏关系。

输入样例:

第一行代表数据组。
每一个数据组第一行为n。
第二行为n个人的指向关系,他们站成了一圈。并且第一个人的右边是第二个人,第一个人的左边是第n个人。

5
4
RLRL
6
LRRRRL
8
RLLRRRLL
12
LLLLRRLRRRLL
5
RRRRR

样例输出:

0
1
1
3
2

样例解释:

In the first test case, players 1 and 2 are attacking each other, and players 3 and 4 are attacking each other. Each player is being attacked by exactly 1 other player, and each player is attacking the player that is attacking them, so all players are already being logical according to Bed Wars strategy and Omkar does not need to talk to any of them, making the answer 0.

In the second test case, not every player acts logically: for example, player 3 is attacked only by player 2, but doesn’t attack him in response. Omkar can talk to player 3 to convert the attack arrangement to LRLRRL, in which you can see that all players are being logical according to Bed Wars strategy, making the answer 1.


【我的思路】

我的思路是,动态规划+枚举。
这道题目,我们可以从第i个人,他指向R或者L,如果这样合法,那么最小代价是多少入手。

让我们稍作分析一下。第i个位置如果是L,或是R,那么合法的可能性有:

i-1ii+1
L或RLR
RLL
LRR
L或RRL

(*理解第三行的RLL,因为如果第i位置是L,且第三行位置是L,那么如果合法,必须要求第i-1个位置是指向R的,要不然i-1指向L,那么这种策略是错误的。)

根据题目要求的游戏规则:
第i项到底填写什么,是根据i-1和i+1两个位置来考虑的。i-1的话,简单。那就是我们之前dp算过的数据罢了。但是i+1这样的位置状态,来自我们的未来。这不是很好解决的问题。因为这样就会存在“后效性”。

动态规划的要求就是:无后效性。

由于第i个位置,考虑未来的状态只有一个i+1的状态,所以小熊想了一个,可以不考虑将来的办法,就是枚举未来的方法。我们可以假设第i+1个位置的指向性。我们只考虑从1开始到i的旋转次数,所以i+1并不影响,1->i的旋转决策。

或者换种说法:我们在计算i的时候,就把未来的i+1所有可能性,全部计算妥当了。

所以我们现在有了一个动态规划转移方法:
dp[i][j][k]:从1考虑到了i,且第i位置上,方向是j(L或者R),并且i+1位置上方向是k(L或者R),最小的旋转方向次数。如果这样的方案不可能实现,那么就是Inf(假装无限大)。

这样的话,由于我们枚举了未来的k,所以这个状态方程转移,只跟i-1位置上的决策有关了。
(题外话:动态规划是怎么计算旋转次数的?例如第i位置上如果是R,那么dp[i][L][*]不管答案是什么,一定是大于等于一的,因为首先把第i位置上的R转为L,就要花费一次的次数。我们就是这样计算,旋转的次数的。那么之前的位置也是同理。)

对于i和i+1分别考虑:
*现在我们设定0代表L,1代表R,即dp[i][0][0]就代表第i位置方向L,i+1位置方向L的最优旋转次数。
得到一个核心的公式:

void doDp() {
    for (int i = 3; i <= n; i++) {
        dp[i][1][1] = dp[i - 1][0][1] + (str[i] != 'R');

        dp[i][1][0] = min(dp[i - 1][0][1], dp[i - 1][1][1]) + (str[i] != 'R');

        dp[i][0][1] = min(dp[i - 1][0][0], dp[i - 1][1][0]) + (str[i] != 'L');

        dp[i][0][0] = dp[i - 1][1][0] + (str[i] != 'L');
    }
}

(str[i] != 'L')意思是,我想在i位置上的方向是L,除了考虑i-1之前的旋转,我还要把自己这个位置如果不是L,那就再旋转一次。

我们来看看,dp[i][1][1] dp[i][1][0]
如果当前i位置填的是R,并且i+1位置填的是R,那么i-1位置要是合法只能填写L。所以从dp[i - 1][0][1]转移。
如果当前位置填写的是R,并且后一位是L。那么i-1位置即可填写L,也可以填写R,所以从他们两个方向转移。

以上,就是核心思路了。

让我们来考虑,末尾和开头改怎么接在一起。

我们现在计算完毕了dp[N][L][L] dp[N][L][R] dp[N][R][L] dp[N][R][R]

他们的含义是,计算完最后一位N,当前填写的是L/R 且下一位(也就是1号人,因为是一个环)是L/R。

那么,由于不清楚一号人,右边是L还是R。所以不太好使用计算完的这四个,进行动态规划。

所以,我们再枚举,一号人和二号人的四种组合,分别计算动态规划到N。这样得到的dp[N][*][*]我们就知道该怎么拼接啦~

( •̀ ω •́ )✧

void solve() {
    LL ans = LInf;
    LL nowAns = 0;  //现在需要的转换次数

    // R R 枚举(1)(2)位置上的状态
    memset(dp, Inf, sizeof(dp));
    nowAns = (str[1] != 'R') + (str[2] != 'R');
    dp[2][1][0] = 0;
    doDp();
    nowAns += dp[n][0][1];
    ans = min(ans, nowAns);

    // R L
    memset(dp, Inf, sizeof(dp));
    nowAns = (str[1] != 'R') + (str[2] != 'L');
    dp[2][0][1] = dp[2][0][0] = 0;
    doDp();
    nowAns += min(dp[n][0][1], dp[n][1][1]);
    ans = min(ans, nowAns);

    // L R
    memset(dp, Inf, sizeof(dp));
    nowAns = (str[1] != 'L') + (str[2] != 'R');
    dp[2][1][0] = dp[2][1][1] = 0;
    doDp();
    nowAns += min(dp[n][0][0], dp[n][1][0]);
    ans = min(ans, nowAns);

    // L L
    memset(dp, Inf, sizeof(dp));
    nowAns = (str[1] != 'L') + (str[2] != 'L');
    dp[2][0][1] = 0;
    doDp();
    nowAns += dp[n][1][0];
    ans = min(ans, nowAns);

    cout << ans << endl;
}

代码参考:

#include <cmath>
#include <cstring>
#include <iostream>
using namespace std;
#define Max_N 200003
#define LInf 0x3f3f3f3ff3f3f3f
#define Inf 0x3f3f3f3f
typedef long long LL;
int T, n;
short s[Max_N];
char str[Max_N];
static LL dp[Max_N + 1][2 + 1][2 + 1];
void doDp() {
    for (int i = 3; i <= n; i++) {
        memset(dp[i], Inf, sizeof(dp[i]));

        dp[i][1][1] = dp[i - 1][0][1] + (str[i] != 'R');
        dp[i][1][1] = min(LInf, dp[i][1][1]);

        dp[i][1][0] = min(dp[i - 1][0][1], dp[i - 1][1][1]) + (str[i] != 'R');
        dp[i][1][0] = min(LInf, dp[i][1][0]);

        dp[i][0][1] = min(dp[i - 1][0][0], dp[i - 1][1][0]) + (str[i] != 'L');
        dp[i][0][1] = min(LInf, dp[i][0][1]);

        dp[i][0][0] = dp[i - 1][1][0] + (str[i] != 'L');
        dp[i][0][0] = min(LInf, dp[i][0][0]);
    }
}
void solve() {
    LL ans = LInf;
    LL nowAns = 0;  //现在需要的转换次数

    // R R 枚举(1)(2)位置上的状态
    memset(dp[1], Inf, sizeof(dp[1]));
    memset(dp[2], Inf, sizeof(dp[2]));

    nowAns = (str[1] != 'R') + (str[2] != 'R');
    dp[2][1][0] = 0;
    doDp();
    nowAns += dp[n][0][1];
    ans = min(ans, nowAns);

    // R L
    memset(dp[1], Inf, sizeof(dp[1]));
    memset(dp[2], Inf, sizeof(dp[2]));

    nowAns = (str[1] != 'R') + (str[2] != 'L');
    dp[2][0][1] = dp[2][0][0] = 0;
    doDp();
    nowAns += min(dp[n][0][1], dp[n][1][1]);
    ans = min(ans, nowAns);

    // L R
    memset(dp[1], Inf, sizeof(dp[1]));
    memset(dp[2], Inf, sizeof(dp[2]));

    nowAns = (str[1] != 'L') + (str[2] != 'R');
    dp[2][1][0] = dp[2][1][1] = 0;
    doDp();
    nowAns += min(dp[n][0][0], dp[n][1][0]);
    ans = min(ans, nowAns);

    // L L
    memset(dp[1], Inf, sizeof(dp[1]));
    memset(dp[2], Inf, sizeof(dp[2]));

    nowAns = (str[1] != 'L') + (str[2] != 'L');
    dp[2][0][1] = 0;
    doDp();
    nowAns += dp[n][1][0];
    ans = min(ans, nowAns);

    //
    cout << ans << endl;
}

int main() {
    std::ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    cin >> T;
    while (T--) {
        cin >> n;
        cin >> (str + 1);
        solve();
    }
    return 0;
}

(已通过)
注意:memset的时间复杂度是O(N)。所以把memset放在dp()里面进行单独的memset,可以加快代码运行速度,第一次拜托同学测试,因为memset的存在,使得代码TLE了,淦。


找规律解法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值