2022牛客寒假基础训练营第6场记录

题目链接
题解链接

A-回文大师(KMP算法)

这题一开始的思路是先将与a[1]相同的位置全部记录在一条链表中,每次i加一的时候遍历链表,判断该位置的前一个字符与a[i]是否相同,相同则将该位置减一,否则删除这个结点。遍历完后输出链表的size,但是这种方法不稳定,最坏的情况下可以到达O(n2),比如所有字符都相同的时候,面对1e6的数据规模显然是不行的。
看了题解后不得不感叹方法的巧妙,KMP算法居然还能这么用,看来还是自己对算法的熟练度不够啊。
a为输入字符串,b为a的反转字符串,对a进行next数组的处理,然后在b中进行匹配。关键的一步来了: 用cnt数组将j出现的值进行计数。cnt[x]表示j=x出现的次数。匹配完后的cnt数组显然不是最终答案,需要将遗漏的情况加上。因此再来一个循环

for (int i = n; i >= 1; i--)
    cnt[nex[i]] += cnt[i];

解释:如果在 i 之前还有字符等于a[i],那么a[i]是包含最近那个字符的情况的,因此用next数组找到那个位置,将cnt[i]的情况加到cnt[next[i]]上。

AC代码:

//345ms
#include <bits/stdc++.h>
#define mem(a, v) memset(a, v, sizeof(a))
using namespace std;
const int N = 1e6 + 5;
int n, a[N], b[N], nex[N], cnt[N];
int main()
{
    scanf("%d", &n);
    mem(cnt, 0);
    for (int i = 1; i <= n; i++)
        scanf("%d", &a[i]);
    nex[0] = -1;
    for (int i = 1; i <= n; i++)
    {
        int k = nex[i - 1];
        while (k != -1 && a[k + 1] != a[i])
            k = nex[k];
        nex[i] = k + 1;
    }
    for (int i = 1; i <= n; i++)
        b[i] = a[i];
    reverse(b + 1, b + 1 + n);
    for (int i = 1, j = 0; i <= n; i++)
    {
        while (j != -1 && a[j + 1] != b[i])
            j = nex[j];
        j++;
        cnt[j]++;
    }
    for (int i = n; i >= 1; i--)
        cnt[nex[i]] += cnt[i];
    for (int i = 1; i <= n; i++)
        printf(i == n ? "%d\n" : "%d ", cnt[i]);
}

B价值序列

之前的一场训练营中做到了类似的题目,那一题是找升序和降序的数字段。这题其实比那题简单,但是因为一些原因没有AC,赛后看题解发现与自己的做法不同的点在于我是找一段升序和降序的段,而题解是找一段相同的数字然后再进行判断去掉后会不会影响结果,可能我的做法在处理相同的数字段的时候有一点问题 但是还没找到特例

下面谈谈正确的做法: 在找到了相同的一段后先判断是否位于开头或者结尾,如果是,那肯定不能全部去掉,得留下一个,因此对结果的贡献是 2length -1,length为这段的长度。如果不是开头或结尾就判断加上前后两个数字后这段是否单调,如果不单调,也是不能全部去掉的,贡献也是2length -1,反之贡献是2length

AC代码:

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

int a[N], p2[N];
int main()
{
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    p2[0] = 1;
    for (int i = 1; i < N; i++) //预处理2的次幂,也可以用快速幂,速度相差不大
        p2[i] = p2[i - 1] * 2 % mod;
    int t, n;
    cin >> t;
    while (t--)
    {
        long long ans = 1;
        cin >> n;
        for (int i = 1; i <= n; i++)
            cin >> a[i];
        for (int i = 1; i <= n; i++)
        {
            int j = i;
            while (j <= n && a[j] == a[j + 1]) //找相同的数字段
                j++;
            //判断是否为开头或结尾,然后判断单调
            if (i != 1 && j != n && (a[i - 1] < a[i] && a[j + 1] > a[i] || a[i - 1] > a[i] && a[j + 1] < a[i]))
                ans = ans * p2[j - i + 1] % mod;
            else
                ans = ans * (p2[j - i + 1] - 1 + mod) % mod;
            i = j;
        }
        cout << ans << endl;
    }
    return 0;
}

G 迷宫2(搜索,bfs,双向队列)

还是第一次做到这种双向搜索的题
这题带点思维,其实还是用bfs找最短路径的问题,只不过对于顺路的点(某点的方向对应的下一个点就是顺路的点)优先搜索,因为花费为0 。因此创建一个双端队列,将顺路的点添加到队首,将不顺路的点添加到队尾,将更新的点打上标记,可以证明每个点最多被入队四次(对应上下左右四个点),因此复杂度是可接受的 O(mn) 。

AC代码:

//162ms
#include <bits/stdc++.h>
#define N 1005
#define mem(a, v) memset(a, v, sizeof(a))
#define fre(f) freopen(f ".in", "r", stdin), freopen(f ".out", "w", stdout)
#define inf 0x3f3f3f3f
using namespace std;

char s[N][N], dir[5] = "RDLU";
int dx[4] = {0, 1, 0, -1}, dy[4] = {1, 0, -1, 0}; // x为行号,y为列号
bool vis[N][N];
int len[N][N], pre[N][N]; //pre[x][y]表示上一个点走到x,y的方向

int main()
{
    // fre("G");
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int t;
    cin >> t;
    while (t--)
    {
        mem(vis, 0);
        mem(len, inf);
        len[1][1] = 0;
        int n, m;
        cin >> n >> m;
        for (int i = 1; i <= n; i++)
            cin >> s[i] + 1;
        deque<int> qx, qy;
        qx.push_front(1);
        qy.push_front(1);
        while (qx.size())
        {
            int x = qx.front();
            int y = qy.front();
            qx.pop_front();
            qy.pop_front();
            if (vis[x][y]) continue;
            vis[x][y] = 1;
            for (int i = 0; i < 4; i++)
            {
                int xx = x + dx[i];
                int yy = y + dy[i];
                if (xx < 1 || xx > n || y < 1 || y > m) continue;
                //如果不是顺路的点花费要加一
                int tlen = len[x][y] + (s[x][y] != dir[i]);
                if (tlen < len[xx][yy])
                {
                    len[xx][yy] = tlen;
                    pre[xx][yy] = i;

                    if (tlen == len[x][y]) //该点为顺路点
                        qx.push_front(xx), qy.push_front(yy);
                    else //该点非顺路点
                        qx.push_back(xx), qy.push_back(yy);
                }
            }
        }
        int px = n, py = m;
        cout << len[n][m] << endl;
        while (px != 1 || py != 1)
        {
            //因为是反着走,所以这里是减去dx和dy
            int tx = px - dx[pre[px][py]], ty = py - dy[pre[px][py]];
            //两个点的花费不同 说明要改方向
            if (len[tx][ty] != len[px][py]) cout << tx << " " << ty << " " << dir[pre[px][py]] << endl;
            px = tx;
            py = ty;
        }
    }
    return 0;
}

J组合数问题

因为输入就只有0 1 2 三种情况,而0不用考虑。如果有n个1,m个2,选k个相乘,可以从1和2中各选a和k-a个,然后用组合数计算即可。

涉及知识:逆元,快速求1~n!逆元

//快速求解逆元的公式 
//说明:inv[i]为i!的逆元
inv[0] = inv[1] = 1;
for (int i = 2; i < N; i++)
    inv[i] = (mod - mod / i) * inv[mod % i] % mod;
for (int i = 2; i < N; i++)
    inv[i] = inv[i - 1] * inv[i] % mod;

AC代码

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

long long f[N], inv[N], p2[N];
inline long long cc(int a, int b)
{
    if (a < b) return 0;
    return f[a] * inv[a - b] % mod * inv[b] % mod;
}

int main()
{
    //快速求逆元初始化
    inv[0] = inv[1] = p2[0] = f[0] = f[1] = 1;
    p2[1] = 2;
    for (int i = 2; i < N; i++)
    {
        f[i] = f[i - 1] * i % mod;
        //快速求逆元第一步
        inv[i] = (mod - mod / i) * inv[mod % i] % mod;
        p2[i] = p2[i - 1] * 2 % mod;
    }
    //快速求逆元第二步
    for (int i = 2; i < N; i++)
        inv[i] = inv[i - 1] * inv[i] % mod;
    int n, k, cnt1 = 0, cnt2 = 0;
    long long ans = 0;
    char c;
    cin >> n >> k;
    getchar();
    for (int i = 0; i < n; i++)
    {
        c = getchar();
        if (c == '1') cnt1++;
        if (c == '2') cnt2++;
        getchar();
    }
    for (int i = 0; i <= k; i++)
    {
        ans += cc(cnt1, i) * cc(cnt2, k - i) % mod * p2[k - i] % mod;
        ans %= mod;
    }
    cout << ans;
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值