DP小练(cf,atcoder)

F - Reordering

链接: link.

题意:

给定长度为 N N N的只包含小写字母的字符串 S S S,现在问有多少个字符串是由 S S S的子序列内的字符串排列形成的

思路:

d p ( i , j ) dp(i,j) dp(i,j)定义为用了前 i i i个字母(字母表中的字母,最多为 26 26 26个),组成长度为 j j j的字符串的数量
d p ( i , j ) = ∑ k = 0 m i n ( j , c n t [ i ] ) d p ( i − 1 , j − k ) × C j k dp(i,j)=\sum_{k=0}^{min(j,cnt[i])} dp(i-1,j-k)×C_{j}^{k}\qquad dp(i,j)=k=0min(j,cnt[i])dp(i1,jk)×Cjk
现在要往前 i − 1 i-1 i1个字母长度为 j − k j-k jk的字符串中插入 k k k个第 i i i个字母了,由于之前的序列顺序通过排列组合已经有顺序了,所以只需要在长度 j j j的情况下,确定 k k k个插入的字母的相对位置,所以就是 C j k C_{j}^{k} Cjk

#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 10;
#define int long long
const int mod = 998244353;
int dp[30][N];
string s;
int cnt[30];
int fact[N], infact[N];

int qmi(int a, int k) {
    int res = 1;
    while (k) {
        if (k & 1) res = res * a % mod;
        k >>= 1;
        a = a * a % mod;
    }
    return res;
}
void init() {
    fact[0] = infact[0] = 1;
    for (int i = 1; i < N; i++) {
        fact[i] = fact[i - 1] * i % mod;
        infact[i] = infact[i - 1] * qmi(i, mod - 2) % mod;
    }
}
int C(int n, int m) {
    if (m > n) return 0;
    return fact[n] * infact[m] % mod * infact[n - m] % mod;
}
signed main() {
    init();

    cin >> s;
    int len = s.length();

    for (int i = 0; i < len; i++) {
        cnt[s[i] - 'a' + 1]++;
    }
    dp[0][0] = 1;
    for (int i = 1; i <= 26; i++) {
        for (int j = 0; j <= len; j++) {
            for (int k = 0; k <= min(j, cnt[i]); k++) {
                dp[i][j] += dp[i - 1][j - k] * C(j, k) % mod;
                dp[i][j] %= mod;
            }
        }
    }

    int res = 0;
    for (int i = 1; i <= len; i++) {
        res += dp[26][i];
        res %= mod;
    }
    cout << res << endl;
}

F - Variety of Digits

链接: link.

题意:

给定数字 M M M个数字
现在问 1 − N 1-N 1N中所有包含这 M M M个数字且没有前导 0 0 0的数字的和

思路:

定义 d p ( 0 , 0 / 1 , s t a t e ) dp(0,0/1,state) dp(0,0/1,state)为选择的数字在二进制下位 s t a t e state state情况下,现在选出来的数字是从高位到枚举的位置,是否小于对应位置的数字 N N N的个数
定义 d p ( 1 , 0 / 1 , s t a t e ) dp(1,0/1,state) dp(1,0/1,state)为选择的数字在二进制下位 s t a t e state state情况下,现在选出来的数字是从高位到枚举的位置,是否小于对应位置的数字 N N N的和
x x x为枚举的数字,循环是从高位往低位进行
n d p ndp ndp数组起到滚动数组的作用
这里选的数,是指 0 − 9 0-9 09这些数
那么此时 n d p ( 0 , f l a g ∣ ∣ x < a [ i ] , s t a t e ∣ 1 < < x ) + = d p ( 0 , f l a g , s t a t e ) ndp(0,flag||x<a[i],state|1<<x)+=dp(0,flag,state) ndp(0,flagx<a[i],state1<<x)+=dp(0,flag,state)
n d p ( 1 , f l a g ∣ ∣ x < a [ i ] , s t a t e ∣ 1 < < x ) + = d p ( 1 , f l a g , s t a t e ) ∗ 10 + d p ( 0 , f l a g , s t a t e ) ∗ x ndp(1,flag||x<a[i],state|1<<x)+=dp(1,flag,state)*10+dp(0,flag,state)*x ndp(1,flagx<a[i],state1<<x)+=dp(1,flag,state)10+dp(0,flag,state)x
如果是枚举的最高位置,枚举时,跳过 0 0 0即可
由于是滚动数组,所以更新完后,再把 n d p ndp ndp赋值给 d p dp dp即可

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int mod = 998244353;

string s;
int m;
int dp[2][2][1 << 10];

signed main() {
    cin >> s;
    int n = s.size();
    vector<int> a(n);
    for (int i = 0; i < n; i++) {
        a[i] = s[n - i - 1] - '0';
    }
    cin >> m;

    int mask = 0;
    for (int i = 0; i < m; i++) {
        int x;
        cin >> x;
        mask |= 1 << x;
    }

    for (int i = n - 1; i >= 0; i--) {
        int ndp[2][2][1 << 10];
        memset(ndp, 0, sizeof(ndp));
        for (int x = 1; x <= (i == n - 1 ? a[i] : 9); x++) {
            (ndp[0][i < n - 1 || x < a[i]][1 << x] += 1ll) %= mod;

            (ndp[1][i < n - 1 || x < a[i]][1 << x] += x) %= mod;
        }
        for (int l = 0; l < 2; l++) {
            for (int s = 0; s < (1 << 10); s++) {
                for (int x = 0; x <= (l ? 9 : a[i]); x++) {
                    (ndp[0][l || x < a[i]][s | 1 << x] += dp[0][l][s]) %= mod;
                    (ndp[1][l || x < a[i]][s | 1 << x] += dp[1][l][s] * 10 + dp[0][l][s] * x) %= mod;
                }
            }
        }
        memcpy(dp, ndp, sizeof(dp));
    }
    int res = 0;
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < (1 << 10); j++) {
            if ((j & mask) == mask) {
                res += dp[1][i][j];
                res %= mod;
            }
        }
    }
    cout << res << endl;
}

数位 d p dp dp+记忆化的思路,基本差不多
d f s ( p o s , s t a t e , l e a d , l i m i t ) dfs(pos,state,lead,limit) dfs(pos,state,lead,limit)代表枚举到了 p o s pos pos位置,选择的数状态为 s t a t e state state,是否有前导 0 0 0,选出来的数直到枚举位置是否小于 N N N
这里选出的那些数,相当于选的是那几个数是题目给的那几个,而不是全部的 0 − 9 0-9 09
并用 p a i r pair pair来分别存符合的个数和求出来的和

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e4 + 10;
const int mod = 998244353;
const int M = 12;
typedef pair<int, int> PII;
char s[N];
int po10[N];
int dex[M];
int m, len, n;
int all;
int dpS[N][1111], dpC[N][1111];
void init() {
    po10[0] = 1;
    for (int i = 1; i < N; i++) {
        po10[i] = po10[i - 1] * 10ll % mod;
    }
    memset(dex, -1, sizeof(dex));
    memset(dpS, -1, sizeof(dpS));
}
PII dfs(int pos, int state, int lead, int limit) {
    if (pos > len) {
        if (lead == 0 && state == all) {
            return (PII){0, 1};
        } else {
            return (PII){0, 0};
        }
    }
    if (!lead && !limit && dpS[pos][state] != -1) {
        return {dpS[pos][state], dpC[pos][state]};
    }
    int res1 = 0, res2 = 0;
    int up = limit ? s[pos] - '0' : 9;

    for (int i = 0; i <= up; i++) {
        int Newstate = state;
        if (dex[i] != -1 && (i || !lead)) {
            Newstate |= (1 << dex[i]);
        }

        PII ans = dfs(pos + 1, Newstate, lead && (i == 0), limit && (i == up));

        (res2 += ans.second) %= mod;
        (res1 += ans.first % mod + ans.second * i % mod * po10[len - pos]) %= mod;
    }
    if (!lead && !limit) {
        dpS[pos][state] = res1;
        dpC[pos][state] = res2;
    }
    return (PII){res1, res2};
}
signed main() {
    init();
    scanf("%s", s + 1);
    scanf("%lld", &n);
    len = strlen(s + 1);
    all = (1 << n) - 1;
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        dex[x] = i - 1;
    }
    cout << dfs(1, 0, 1, 1).first << endl;
}

链接: link.

E. Masha-forgetful

题意:

给定一个由长度为 N N N数字组成的字符串,现在又给 M M M个别的字符串,现在问能否用其他字符串的长度至少 2 2 2的子段来组成这个长度为 N N N的字符串,能得话,输出挑选的字段

思路:

首先任何一个大于 2 2 2的数字,都可以被表示成若干个 2 2 2 3 3 3的和
那么你就把输入的字符串拆成长度为 2 2 2 3 3 3的字段
那么就可以定义 d p ( i ) dp(i) dp(i)为到 i i i为止,字符串是否匹配
那么只要 d p ( i − 2 ) = 1 dp(i-2)=1 dp(i2)=1 s [ i − 1 ] s [ i ] s[i-1]s[i] s[i1]s[i]这个字段存在,那么 d p ( i ) = 1 dp(i)=1 dp(i)=1
那么只要 d p ( i − 3 ) = 1 dp(i-3)=1 dp(i3)=1 s [ i − 2 ] s [ i − 1 ] s [ i ] s[i-2]s[i-1]s[i] s[i2]s[i1]s[i]这个字段存在,那么 d p ( i ) = 1 dp(i)=1 dp(i)=1
最后只要 d p ( N ) = 1 dp(N)=1 dp(N)=1,那就从后往前还原路径就可以,提前预处理好每个子段所对应的为止就行
存位置时,可以用 t u p l e tuple tuple三元组来存,代码量会减少一些

#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10;
struct node {
    int x, y, z;
};
int n, m;
bool f[N];
char s[N];
int main() {
    cout.tie(0)->sync_with_stdio(0);
    int T;
    cin >> T;
    while (T--) {
        cin >> n >> m;
        node a[10][10] = {0, 0, 0};
        node b[10][10][10] = {0, 0, 0};
        for (int i = 0; i < m + 2; i++) {
            f[i] = 0;
        }
        for (int i = 1; i <= n; i++) {
            cin >> s + 1;
            for (int j = 1; j <= m; j++) {
                if (j + 1 <= m) {
                    a[s[j] - '0'][s[j + 1] - '0'] = {j, j + 1, i};
                }
                if (j + 2 <= m) {
                    b[s[j] - '0'][s[j + 1] - '0'][s[j + 2] - '0'] = {j, j + 2, i};
                }
            }
        }
        cin >> s + 1;
        f[0] = 1;
        int x, y, z;
        for (int i = 1; i <= m; i++) {
            if (i >= 2) {
                x = a[s[i - 1] - '0'][s[i] - '0'].x;
                y = a[s[i - 1] - '0'][s[i] - '0'].y;
                z = a[s[i - 1] - '0'][s[i] - '0'].z;
                if (f[i - 2] == 1 && x != 0 && y != 0 && z != 0) {
                    f[i] = 1;
                }
            }
            if (i >= 3) {
                x = b[s[i - 2] - '0'][s[i - 1] - '0'][s[i] - '0'].x;
                y = b[s[i - 2] - '0'][s[i - 1] - '0'][s[i] - '0'].y;
                z = b[s[i - 2] - '0'][s[i - 1] - '0'][s[i] - '0'].z;
                if (f[i - 3] == 1 && x != 0 && y != 0 && z != 0) {
                    f[i] = 1;
                }
            }
        }

        if (f[m] == 0) {
            cout << "-1" << endl;
        } else {
            vector<node> ans;
            int i = m;
            int x, y, z;
            while (i) {
                x = a[s[i - 1] - '0'][s[i] - '0'].x;
                y = a[s[i - 1] - '0'][s[i] - '0'].y;
                z = a[s[i - 1] - '0'][s[i] - '0'].z;
                if (i >= 2 && f[i - 2] && x != 0 && y != 0 && z != 0) {
                    ans.push_back({x, y, z});
                    i -= 2;
                } else {
                    x = b[s[i - 2] - '0'][s[i - 1] - '0'][s[i] - '0'].x;
                    y = b[s[i - 2] - '0'][s[i - 1] - '0'][s[i] - '0'].y;
                    z = b[s[i - 2] - '0'][s[i - 1] - '0'][s[i] - '0'].z;
                    ans.push_back({x, y, z});
                    i -= 3;
                }
            }
            reverse(ans.begin(), ans.end());
            cout << ans.size() << endl;
            for (int i = 0; i < ans.size(); i++) {
                cout << ans[i].x << " " << ans[i].y << " " << ans[i].z << endl;
            }
        }
    }
}

To be continued
如果你有任何建议或者批评和补充,请留言指出,不胜感激

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值