Codeforces Round 662 (Div. 2) #D. Rarity and New Dress (二维dp / bitset卡常)

原题链接:D. Rarity and New Dress


题目大意:


给出一个 n × m n \times m n×m 带有小写字母的网格,要你找出有多少个菱形(如题中图片所示)满足以下条件:

  • 圈起的菱形内部所有字母都必须相同。
  • 菱形不能超过网格大小。
  • 一个格子也算作一个菱形。

问:对于给出的网格,总共有多少种不同的方法构成一个菱形。

解题思路:


做法 1 1 1 :二维DP


对于一个格子,我们考虑从上下左右四个方向进行转移。

对一个菱形来说,我们假设其上下左右四个方向格子的长度为 k k k 。那么对于单个格子构成的棱形来说 k = 0 k = 0 k=0 ,单个格子加周围四个格子构成的一个小菱形来说 k = 1 k = 1 k=1,以此类推。

观察一下,一个边长为 k k k 的菱形能够构成的前提是其四周可以分成四个边长为 k − 1 k - 1 k1 的小棱形。


在这里插入图片描述
(上图黄色为 k = 3 k = 3 k=3 阶菱形的中心,深蓝色为 k = 2 k = 2 k=2 阶小菱形的中心)


那么我们可以先考虑上下方向最大能够扩展的长度,然后再考虑左右方向能够扩展的长度。

  • u p [ i ] [ j ] up[i][j] up[i][j] 表示坐标为( i , j i,j i,j )的方格能最大向上扩展的长度。
  • d o w n [ i ] [ j ] down[i][j] down[i][j] 表示坐标为( i , j i,j i,j )的方格能最大向下扩展的长度。
  • l e f t [ i ] [ j ] left[i][j] left[i][j] r i g h t [ i ] [ j ] right[i][j] right[i][j] 同理。

那么转移方程很好想,几乎都是以下形式:
u p [ i ] [ j ] = { u p [ i − 1 ] [ j ] + 1 (str[i][j] = str[i - 1][j]), 0 (str[i][j] ≠ str[i - 1][j]) , up[i][j] = \begin{cases} up[i - 1][j] + 1 \quad \text {(str[i][j] = str[i - 1][j])} ,\\ 0 \quad \quad \quad \quad \quad \quad \quad \text{(str[i][j]$\not=$str[i - 1][j])},\\ \end{cases} up[i][j]={up[i1][j]+1(str[i][j] = str[i - 1][j])0(str[i][j]=str[i - 1][j])

对于 l e f t left left r i g h t right right 来说,我们要额外考虑 u p up up d o w n down down 的影响:
l e f t [ i ] [ j ] = { min ⁡ ( u p [ i ] [ j ] , d o w n [ i ] [ j ] , l e f t [ i ] [ j − 1 ] + 1 ) (str[i][j] = str[i][j - 1]), 0  (str[i][j] ≠ str[i - 1][j]) , left[i][j] = \begin{cases} \min(up[i][j], down[i][j], left[i][j - 1] + 1) \quad \text {(str[i][j] = str[i][j - 1])} ,\\ 0 \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \text{ (str[i][j]$\not=$str[i - 1][j])},\\ \end{cases} left[i][j]={min(up[i][j],down[i][j],left[i][j1]+1)(str[i][j] = str[i][j - 1])0 (str[i][j]=str[i - 1][j])

这是因为我们要向左和向右扩展,前提是我们上下能够扩展。最终答案就可以在 l e f t left left r i g h t right right 中取得了。
 
a n s = n ∗ m + ∑ i = 1 n ∑ j = 1 m min ⁡ ( l e f t [ i ] [ j ] , r i g h t [ i ] [ j ] ) 。 ans = n * m + \sum_{i =1}^{n}\sum_{j =1}^{m}\min(left[i][j],right[i][j])。 ans=nm+i=1nj=1mmin(left[i][j],right[i][j])
 
时间复杂度: O ( n m ) O(nm) O(nm)

AC代码:


#include <bits/stdc++.h>
#define YES return void(cout << "Yes\n")
#define NO return void(cout << "No\n")
using namespace std;

using u64 = unsigned long long;
using PII = pair<int, int>;
using i64 = long long;

void solve() {

    int n, m;
    cin >> n >> m;
    vector<string> str(n + 2);
    for (int i = 1; i <= n; ++i) {
        cin >> str[i];
        str[i] = '?' + str[i] + '?';
    }

    vector<vector<i64>> up(n + 1, vector<i64>(m + 1, 0));
    auto down = up, left = up, right = up;

    for (int i = 2; i <= n - 1; ++i) {
        for (int j = 2; j <= m - 1; ++j) {
            int t = n - i + 1;
            if (str[i][j] == str[i - 1][j]) {
                up[i][j] = up[i - 1][j] + 1;
            }
            if (str[t][j] == str[t + 1][j]) {
                down[t][j] = down[t + 1][j] + 1;
            }
        }
    }

    for (int i = 2; i <= n - 1; ++i) {
        for (int j = 2; j <= m - 1; ++j) {
            int t = m - j + 1;
            if (str[i][j] == str[i][j - 1]) {
                left[i][j] = min({up[i][j], down[i][j], left[i][j - 1] + 1});
            }
            if (str[i][t] == str[i][t + 1]) {
                right[i][t] = min({up[i][t], down[i][t], right[i][t + 1] + 1});
            }
        }
    }

    i64 ans = n * m;
    for (int i = 2; i <= n - 1; ++i) {
        for (int j = 2; j <= m - 1; ++j) {
            ans += min(left[i][j], right[i][j]);
        }
    }

    cout << ans << '\n';
}

signed main() {

    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    int t = 1; //cin >> t;
    while (t--) solve();

    return 0;
}

 

做法 2 2 2 : bitset卡常


假设我们进行最暴力的做法,利用做法 1 1 1 的思路:

( 一个边长为 k k k 的菱形能够构成的前提是其四周可以分成四个 k − 1 k - 1 k1 小棱形 )

我们设计这样一个 0 / 1 0/1 0/1 状态: d p [ k ] [ i ] [ j ] dp[k][i][j] dp[k][i][j] 表示坐标为( i , j i,j i,j)的点能否构成长度为 k k k 的菱形,为 1 1 1 则可以构成,为 0 0 0 则不能构成。

那么状态转移方程就是:
 
d p [ k ] [ i ] [ j ] = d p [ k − 1 ] [ i − 1 ] [ j ] dp[k][i][j] = dp[k-1][i -1][j] dp[k][i][j]=dp[k1][i1][j] & \& & d p [ k − 1 ] [ i + 1 ] [ j ] dp[k-1][i+1][j] dp[k1][i+1][j] & \& & d p [ k − 1 ] [ i ] [ j − 1 ] dp[k-1][i][j-1] dp[k1][i][j1] & \& & d p [ k − 1 ] [ i ] [ j + 1 ] dp[k-1][i][j+1] dp[k1][i][j+1]
 
即上下左右都可以构成 k − 1 k-1 k1 阶的菱形时,( i , j i,j i,j)的点就可以构成 k k k 阶的菱形。

最后,对于每一个 k k k ,我们算出一共有多少个 1 1 1 即可。
 
a n s = ∑ i = 1 n ∑ j = 1 m ∑ k = 1 min ⁡ ( n , m ) 2 d p [ k ] [ i ] [ j ] ans = \sum_{i =1}^{n}\sum_{j =1}^{m}\sum_{k=1}^{{\min(n,m) \over 2}}dp[k][i][j] ans=i=1nj=1mk=12min(n,m)dp[k][i][j]
 

O ( n 3 ) O(n^{3}) O(n3) 的复杂度是过不了的,但我们发现 d p dp dp 数组中都是对 0 / 1 0/1 0/1 进行维护,考虑用 b i t s e t bitset bitset 优化。

b i t s e t bitset bitset 来代替我们的 d p dp dp 数组,然后利用类似滚动数组的方法从 k − 1 k - 1 k1 阶中获得 k k k 阶的状态。

先处理出 k = 1 k=1 k=1 时的 d p dp dp 数组 0 / 1 0/1 0/1 状态存入一个 b i t s e t bitset bitset 内。

预处理之后,那么后面 2 , 3 , . . . , k 2,3,...,k 2,3,...,k 阶的状态转移方程为:
 
d p [ n x t ] [ i ] = ( d p [ c u r ] [ i ] < < 1 ) & ( d p [ c u r ] [ i ] > > 1 ) & d p [ c u r ] [ i − 1 ] & d p [ c u r ] [ i + 1 ] dp[nxt][i] = (dp[cur][i] << 1) \& (dp[cur][i] >> 1) \& dp[cur][i - 1] \& dp[cur][i + 1] dp[nxt][i]=(dp[cur][i]<<1)&(dp[cur][i]>>1)&dp[cur][i1]&dp[cur][i+1] (当前第 k k k 阶状态 = 第 k − 1 k - 1 k1 阶: 左 & \& & & \& & & \& & 下)
 
(其中 & \& & 代表二进制的与操作, d p [ c u r ] [ i ] dp[cur][i] dp[cur][i] k − 1 k-1 k1 阶第 i i i 行的 d p dp dp 状态, d p [ n x t ] [ i ] dp[nxt][i] dp[nxt][i] k k k 阶第 i i i 行的 d p dp dp 状态)

 

做完这些之后,剩下的就是看测评姬和你是否心灵相通了。
(当然也可以用循环展开等等一些魔幻的方法成为卡常超人,不过我懒就是了)
 

在这里插入图片描述
复杂度: O ( n 3 / w ) O(n^{3} / w) O(n3/w)

注意一些卡常细节还是能卡过去的。

AC代码:


#include <bits/stdc++.h>
#define YES return void(cout << "Yes\n")
#define NO return void(cout << "No\n")
using namespace std;

using u64 = unsigned long long;
using PII = pair<int, int>;
using i64 = long long;

const int N = 2e3;

bitset<N> dp[2][N];

void solve() {
    int n, m;
    cin >> n >> m;
    vector<string> str(n);
    for (auto& s : str) {
        cin >> s;
    }

    i64 ans = n * m;

    //处理出 k = 1 阶的 bitset
    for (int i = 1; i < n - 1; ++i) {
        for (int j = 1; j < m - 1; ++j) {
            char cur = str[i][j];
            dp[0][i][j] = (cur == str[i + 1][j] && cur == str[i - 1][j] && cur == str[i][j + 1] && cur == str[i][j - 1]);
        }
        ans += dp[0][i].count();
    }

    //用res小小优化一下转移次数
    i64 res = 1;

    int cur = 0, nxt = 1, t = min(n, m) >> 1;
    for (int k = 1; res && k <= t; ++k) {
        res = 0;
        for (int i = 1; i < n - 1; ++i) {
            dp[nxt][i] = (dp[cur][i] << 1) & (dp[cur][i] >> 1) & dp[cur][i - 1] & dp[cur][i + 1];
            res += dp[nxt][i].count();
        }
        ans += res;
        swap(cur, nxt);
    }

    cout << ans << '\n';
}

signed main() {

    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    int t = 1; //cin >> t;
    while (t--) solve();

    return 0;
}


感谢观看!


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬味的橙汁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值