钉耙编程round6 个人题解

钉耙编程round6 个人题解

这一场个人看来,至少签到题和铜牌题的难度稍微降了一点

即便是我这种萌新也能做出来五道题目哈哈

好消息,写出来了五道题

坏消息,写出来的题定位全是签到题(破防了哈哈)

但是昨天开始做的时候就产生了一种想法

——该死的mujica还在追我

这场比赛的出题人浓度很高,所有题面都是梗

寒假的牛客训练营也有一场是这样的,xswl

题解的难度是我个人认为的难度递增顺序

1001 烤羊

纯签到,注意一下几个香气值可以都不选

这里实话实话,我后面回来补(队友签了),wa了一发

我是直接手动枚举的,没有考虑三个都不选的情况

还是细节不够好

1003 抹茶

模拟题,按照题意提前把ai + bi相等的区域拿出来

然后用前缀和求美味值之和,因为r - l + 1相对于这个区域所有需要加和的元素来说是相等的

最后用个ans记录一下最大值即可

1007 双相

两个人一个一个跳

注意到数据给出的意思是可以往回跳

那可以往回跳原来的数据顺序就不重要了

这里我们直接把red和black的元素分到两个vector里

然后贪心排序

从高到低一人取一个最大值

这里注意一个顺序问题

因为是Mutsumi先跳,所以最后只有两种情况

最后是Mutsumi结尾,那red的数组取得元素要多一个

反之则两个取得元素数量相等(注意不是black多一个)

怎么取可以直接通过两个vector的size判断

上代码

const int INF = 1e9;
const int N = 2e5 + 10;
​
LL f[N][2];
int a[N];
​
void work() {
    int n;
    cin >> n;
    for (int i = 0; i < n; i ++ ) cin >> a[i];
​
    string s;
    cin >> s;
    vector<int> mu, mo;
    LL sum1 = 0, sum2 = 0;
    for (int i = 0; i < s.size(); i ++ ) {
        if (s[i] == 'R') mu.push_back(a[i]), sum1 += a[i];
        else mo.push_back(a[i]), sum2 += a[i];
    }
    LL ans = 0;
    sort(mu.begin(), mu.end());
    sort(mo.begin(), mo.end());
​
    reverse(mu.begin(), mu.end());
    reverse(mo.begin(), mo.end());
​
    for (int i = 0; i < mo.size() && i < mu.size(); i ++ ) {
        ans += mo[i] + mu[i];
    }
    if (mu.size() > mo.size()) ans += mu[mo.size()];
​
    cout << ans << endl;
​
}

1008 天使

怎么还有eva

这道题应该更像是找性质的题

手动模拟一下

a b c d四个数字

从前往后爆炸的总和是ab + (a + b)c + (a + b + c)d

先爆前两个,后爆后两个,最后一起爆是

ab + cd + (a + b)(c + d)

还有很多别的方式

当时我手动模拟了两个数据,发现S无论如何

总和都是一个固定的值

就是每一个数字乘以其它所有数字的和

最后把这些乘积加起来就是S

那么方案数呢

这里不同的使徒就算能量相同,也是认为是不同的两个使徒

所以我们就不用管能量了

第一次,我们在n个里面选两个爆

第二次,在n - 1(有两个合并了)选两个爆

第三次……

总之,我们的答案是组合数的乘积

\prod_{i=2}^n C_{n}^{2}

1002 英逃

我是先画了一张图模拟了一下

把数字的高低假想成高的台阶和低的台阶

如果你进行了操作,那就是将一块区域的所有地方都变成最高的那一块台阶的高度

然后变平

仔细读完题以后我们有两个地方需要解决

一个是如何快速求出一个区间的最大值(RMQ问题)

一个是如何求出代价,也就是所需最小的区间长度

第一个很容易想到是st表

我当时也是现场看得以前写得代码哈哈

这里尽量用简单的语言给大家介绍一下

st表可以在O(nlogn)的时间完成表的建立(预处理),同时O(1)查询,线段树要O(logn)哦,而且比这难写多了,常数还大

运用的核心思想是倍增

你想,假如你要找一个区间最大值

最暴力的方法是不是一个一个遍历过去,然后找到最大的那个

但是我们要找的不止这一个区间,我们要能快速求出所有的区间

但是如果我们能够去标记一个二维的 l r 数组,那我们建立数组就注定了预处理时间至少为O(n^2)

那我们怎么办呢

答案是倍增

我们预处理的时候就存储从某个数开始,后面2的次方倍的长度区间内的最大值

这个写在代码里面很清楚

for (int i = 1; i <= n; i ++ ) {
        dp[i][0] = ar[i];
}
for(int j = 1; (1 << j) <= n; j ++ )
    for(int i = 1; i + (1 << (j - 1)) <= n; i ++ )
        dp[i][j] = max(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]);

我们dp [i] [j]就是求从i开始长度为2^j范围内的最大值,那我们只要从i开始的2^{j-1}个区间和从i + 2^{j - 1} 开始的 2^{j - 1} 个区间里面取最大值即可

至于查询

我们通过一种很聪明的方式

inline int maxL(int L, int R) {
    int d=LOG[R-L+1];
    return max(dp[L][d],dp[R-(1<<d)+1][d]);
}

这样查询挺违反直觉的,因为中间会有重复的区间

但是你仔细一想,就会觉得非常巧妙,而且把时间控制在了O(1)

我们选择在l的右侧2^{logn(下取整)}里取出左边一部分,同时再取右侧往左2^{logn(下取整)}的一部分的最大值

那我们再来看二分

这里就是二分区间长度

然后从左到右一个一个区间去套

中间计算的时候要注意每一次计算的边界

这个可以仔细看看代码

const int MAXN=1e5 + 10,MAXF=18;
int ar[MAXN];
int dp[MAXN][MAXF];
int LOG[MAXN];
​
inline int maxL(int L, int R) {
    int d=LOG[R-L+1];
    return max(dp[L][d],dp[R-(1<<d)+1][d]);
}
​
void work() {
    int n;
    LL m;
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ) cin >> ar[i];
    for (int i = 1; i <= n; i ++ ) {
        dp[i][0] = ar[i];
    }
​
    for(int j = 1; (1 << j) <= n; j ++ )
        for(int i = 1; i + (1 << (j - 1)) <= n; i ++ )
            dp[i][j] = max(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]);
​
    vector<LL> b(n + 1), pre(n + 1); // 差分和差分的前缀和数组
    for (int i = 2; i <= n; i ++ ) {
        b[i] = abs(ar[i] - ar[i - 1]);
        pre[i] = pre[i - 1] + b[i];
    }
    
    int l = 0, r = n;
​
    while (l < r) {
        int mid = l + r >> 1;
        LL res = 1e18;
        for (int i = 1; i + mid - 1 <= n; i ++ ) { // i ~ i + x - 1
            int ll = i, rr = i + mid - 1;
            int maxx = maxL(ll, rr);
            LL t1, t2;
            if (ll == 1) t1 = 0;
            else t1 = pre[ll - 1] + abs(maxx - ar[ll - 1]); // 左侧的起伏加和
            if (rr == n) t2 = 0;
            else t2 = pre[n] - pre[rr + 1] + abs(maxx - ar[rr + 1]);// 右侧起伏加和
            LL tmp = t1 + t2;
            res = min(tmp, res);
        }
        if (res <= m) r = mid;
        else l = mid + 1;
    }
​
    cout << l << endl;
}

1009 键帽

当时没有做出来这道题

后面看了题解理解了好久(还是有点菜)

不过最后完全自己弄明白怎么做的时候还是非常非常高兴的呢

题解和题解的代码省略了许多东西,所以看起来不明不白的

这里我详细讲一下思路

做法是dp

如果我们想要很好的去划分集合,如果只是随便去多少个连着的为元音,会很难划分

这里有个常见的做法,是将划分作为

f[i] [j] 为i个字母的合法串,最后j个都为元音

我们以最后j个作为判断的标准

然后很自然的可以得到状态转移的方法

f[i] [0] = sum(f[i - 1] [0], f[i - 1] [1], ……f[i - 1] [k]) * 21 (所有可行情况的最后加上一个辅音)

f[i] [j] = f[i - 1] [j - 1] * 5 末尾加一个元音

递推可得

f[i] [len] = f[i - len] [0] * pow(5, len);

可惜这样做是O(n^2) 的

我们有没有其它方法把第二维化小呢

有的,我们只考虑最后一个字母为元音或为辅音的情况

相当于一个状态机

f[i] [1] 代表最后一个字母为元音

f[i] [0] 代表最后一个字母为辅音

f[i] [1] = sum(f[i - 1] [0] * 5 + f[i - 2] [0] * 5^2 + …… + f[i - k] [0] * 5^k) 对应原来的有k个字母结尾的情况

f[i] [0] = (f[i - 1] [0] + f[i - 1] [1]) * 21 不管怎样直接加一个辅音字母

好了,那么问题来了,我们应该如何快速求出f[i] [1]呢

不难发现,所有的数都是乘以一个pow(5, k)

那我们可以试着制造一个前缀和数组

里面装的是f[i] [0] * pow(5, n - i)

这里类比一下

相当于给你一个1 * n的格子

你往里面装字母,装了i个,后面的全部都用元音补齐

我们取出的时候,把后面的多余的元音一刀切掉就可以了

因为求f[i] [1] 里面所有数的 i 和后面5的上标之和是固定的,就是所求f[i] [1] 的i

那我们取出的时候前缀和一减然后除以一个pow(n - i) 就行了

具体可以看代码

const int INF = 1e9;
const int MOD = 1e9 + 7;
const int N = 1e6;
​
int qmi(int a, int k) {
    int res = 1;
    while (k) {
        if (k & 1) res = 1ll * res * a % MOD;
        k >>= 1;
        a = 1ll * a * a % MOD;
    }
​
    return res;
}
vector<int> pow5(N + 1);
vector<int> inv5(N + 1); // 逆元
​
void work() {
    int n, m;
    cin >> n >> m;
    
    vector<vector<LL>> f(n + 1, vector<LL>(2));
    vector<LL> pre(n + 1);
    f[0][0] = 1;
    pre[0] = pow5[n];
    for (int i = 1; i <= n; i ++ ) {
        int l = i - m; 
        // i - m, 代表i为止的m + 1个数,为什么是m + 1个呢,因为dp[i][0]代表的是最后一个为辅音,所以少一个元音凑齐m个
        if (l > 0) f[i][1] = ((pre[i - 1] - pre[l - 1]) + MOD) % MOD;
        else f[i][1] = pre[i - 1];
        f[i][1] = f[i][1] * inv5[n - i] % MOD;
        f[i][0] = (f[i - 1][0] + f[i - 1][1]) * 21 % MOD;
​
        pre[i] = (pre[i - 1] + f[i][0] * pow5[n - i]) % MOD;
    }
    LL ans = 0;
    for (int i = 0; i <= m; i ++ ) {
        ans += f[n - i][0] * pow5[i] % MOD;
        ans %= MOD;
    }
    cout << ans << endl;
​
}
​
int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
​
    // 初始化
    pow5[0] = 1;
    for (int i = 1; i <= N; i ++ )
        pow5[i] = 1ll * pow5[i - 1] * 5 % MOD;
    
    inv5[N] = qmi(pow5[N], MOD - 2);
    for (int i = N - 1; i >= 0; i -- ) {
        inv5[i] = 1ll * inv5[i + 1] * 5 % MOD; // 这样反着乘5可以转化到inv5[i - 1]
    }
​
    int T = 1;
    cin >> T;
    while (T -- ) work();
​
    return 0;
}

现在是4月12日20:43,晚饭还没吃

今天早上打了蓝桥杯,下午因为睡觉睡得比较少,图书馆写不进去作业,所以开始看看书,看书又看得很困,精神不太好

躺在椅子上睡了一觉

醒来好多了,仔细补了一下主要是键帽这题

收获还是蛮大的

现在去吃晚饭啦!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值