记录几个巧妙的dp (ABC307 E)

E - Distinct Adjacent (atcoder.jp)

题意

n个人,排列成一个环形,现在有m种数字,给每个人发放数字,且保证每个人不能和相邻的人有相同数字,问你有多少种方案。

思路

首先可以用组合数解决,我差一些没想懂,所以先不说

其次就是另一种办法环形dp。

设计dp[N][2]
1) dp[i][0]表示第i位,与第一位不同时的情况,
2) 若从dp[i-1][0]转移而来 -> 此时相当于我们在这个位置只能使用n-2个数字(第一位确定,上一位确定,且其二者不同)
若从dp[i-1][1]转移而来 -> 此时相当于我们在这个位置只能使用n-1个数字(第一位确定,上一位确定,且其二者相同)
3) dp[i][1]表示第i位,与第一位相同时的情况,此时本位确定,再加上不能和上一位相同的限制,只能从dp[i-1][0]转移而来

注意:初始的时候,对于dp[1][1],其本身就是第一位,故有m种可能

代码

#include <bits/stdc++.h>

using namespace std;
using i64 = long long;

constexpr i64 MOD = 998244353;

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

    i64 n, m;
    cin >> n >> m;

    vector dp(n + 1, vector<int>(2, 0));

    dp[1][1] = m;
    for (int i = 2; i <= n; i++) {
        dp[i][0] = dp[i - 1][0] * (m - 2) % MOD;
        dp[i][0] = (dp[i][0] + dp[i - 1][1] * (m - 1) % MOD) % MOD;
        dp[i][1] = dp[i - 1][0];
    }
    cout << dp[n][0] << "\n";

    return 0;
}

/*
    设计dp[N][2]
    dp[i][0]表示第i位,与第一位不同时的情况,
    若从dp[i-1][0]转移而来 -> 此时相当于我们在这个位置只能使用n-2个数字(第一位确定,上一位确定,且其二者不同)
    若从dp[i-1][1]转移而来 -> 此时相当于我们在这个位置只能使用n-1个数字(第一位确定,上一位确定,且其二者相同)
    dp[i][1]表示第i位,与第一位相同时的情况,此时本位确定,再加上不能和上一位相同的限制,只能从dp[i-1][0]转移而来
    注意:初始的时候,对于dp[1][1],其本身就是第一位,故有m种可能
*/

由于这个题目的出题人说,它借鉴了ABC232的E题,所以我去做了ABC232E,这个题目相对更难想,有4种大的情况,详情见代码中的注释吧

E - Rook Path (atcoder.jp)

代码

更新3:定义dp[k][i][j]为走k步达到状态i, j的方案数,其中k被滚动数组压掉,原来应该是开K*2*2,滚动掉第一维度变成2*2

注意:可能你会问为什么在上面的转移中dp[0][1] 和 dp[1][0]的系数不为零,而f[1][1]的转移中dp[0][1] 和dp[1][0]的系数为什么是零?

问出这个问题的你和我一样,开始并没有理解这个转移的真正含义

答案其实就是:因为dp[1][0]只需要在列上走动一步就会与y1不同,即摆脱了原有的状态,成为了新的状态dp[1][1] (即从只有行和x1不同的状态10  转移成了 行和列与x1 y1不同的状态11)所以其实dp[1][1] 只需要加上1个dp[1][0],dp[0][1]同理

更新1:由于我的解释过于狗屎,所以贴一个洛谷题解区:AT_abc232_e [ABC232E] Rook Path - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

更新2:我懂了,看了题解区最后一个大佬的图懂的,大概就是 

对于f[1][1],能够转移到这个状态的同一行有m - 1个点,同一列有n - 1个点(但这两堆点中包含了与起点同行或者同列的),我们画图可以发现上述分解(强烈推荐看题解区那个大佬的很多张图,非常清晰,我画的太丑了)

大佬的题解:abc 232 e 题解 - cn 的垃圾桶 - 洛谷博客 (luogu.com.cn)

#include <bits/stdc++.h>

using namespace std;
using i64 = long long;

constexpr int MOD = 998244353;

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

    int n, m, k, sx, sy, ex, ey;
    cin >> n >> m >> k >> sx >> sy >> ex >> ey;

    vector dp(3, vector<i64>(3, 0));
    dp[0][0] = 1;

    for (int i = 1; i <= k; i++) {
        vector f(3, vector<i64>(3, 0));
        f[0][0] = ((n - 1) * dp[1][0] % MOD + (m - 1) * dp[0][1] % MOD) % MOD; 
        f[0][1] = ((m - 2) * dp[0][1] % MOD + (n - 1) * dp[1][1] % MOD + dp[0][0]) % MOD;
        f[1][0] = ((n - 2) * dp[1][0] % MOD + (m - 1) * dp[1][1] % MOD + dp[0][0]) % MOD;
        f[1][1] = ((n - 2 + m - 2) * dp[1][1] % MOD + dp[0][1] + dp[1][0]) % MOD; 
        dp = move(f);
    }

    if (sx == ex && sy == ey) {
        cout << dp[0][0] << "\n";
    } else if (sx == ex) {
        cout << dp[0][1] << "\n";
    } else if (sy == ey) {
        cout << dp[1][0] << "\n";
    } else {
        cout << dp[1][1] << "\n";
    }

    return 0;
}

小结

第一个是环形dp,采用第二维度代表是否与第一项相同,从而达到保证相邻和首尾都不同的目的

第二个是小的状态,合并成大的状态统一进行转移,每次找对应的转移,滚动数组优化

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值