2024暑假牛客多校训练营第二场题解(细述I,H)

E GCD VS XOR

题意:给出x,让你输出一个y使得x异或y等于x和y的最小公约数。

思路:一开始看到会感觉有点麻烦,异或要考虑的二进制位数情况很复杂,细细想会很麻烦,但是转念一想异或也给出一个好处,因为题目要求是随便一个满足条件的数,那么我们不如将思考范围缩小,首先考虑异或,如果我们将y的前几位都和x变得一样,那么我们只需要考虑后几位。那么我们不如只考虑x的最后一位1,因为我们发现gcd的最大公约数肯定会是二进制的第一个1,而异或性质决定了我们只需要让y的同位置为0,别的位置一样,异或出来的就是最大公约数,因此直接输出x-lowbit(x)即可。

C RED Walking on Grid

题意:给出一个2*n的矩阵,矩阵只有w或r,red可以走红格,不能走白格,且走过的红格会变成白格,且red只能上下左右移动。问red最多可以移动几次(red可以从任意位置出发)。

思路:一开始以为是找最大联通,直接跑贪心就行,后面发现不行,有些情况往下走了就不能跑回上面了,于是转用dp思考,如果一个地方可以,那么就从上一块转移过来就行,这一块的值就是这一块的值和上一个地方的值+1取max。最后再整体遍历一下看看哪个地方最大,输出最大减一即可,因为也是签到题所以不细述了。

H Instructions Substring

题意:给出一个长度为n的字符串,a表示左移,d表示右移,w表示上移,s表示下移。问在这个字符串中取一个子串(即连续的一部分),经过这些操作后,能从0,0经过x,y坐标的子串有几个。

思路:一开始读错题意了,以为是选子序列,直接想到组合数去了浪费了很多时间,后来重新读题之后发现是子串,而且题目只是要求经过而不是最终到达某个点,那么就很好解决了,因为我们取的是子串,就是连续的某一部分,然后我们要让这一部分经过x,y点。那我们不妨考虑,我们先让一个子串到x,y点,那么这个子串后面的所有我们都可以随便取,因为这个子串的最后一位已经经过了x,y满足条件。但是如果一段一段的去找又很麻烦而且很浪费时间。那么我们不妨考虑,子串其实就是整串都走,然后去掉前面和后面一部分。后面我们随便取了,那么我们只需要去掉前面不就好了?这个思路是不是很熟悉?没错,就是前缀和,我们先全部都取,然后看看从0,0取到第i个操作走到哪了,再看看这个点离x,y还差了多少,看看前面走过的部分有没有这些差值,我们把这些差值去掉不就相当于到了x,y?因此我们每次走过一个操作,就储存这次操作后的坐标,让那个坐标对应的值加1,然后再去找对当前坐标来说,和x,y差了多少,看看之前有没有走到过这样的差值坐标,走到过几次,把那部分去掉就是我们到x,y需要的子串,然后对于后面的随便取都是答案,贡献就是差值坐标的数量*i后面的串的长度(包括i本身别忘了)。重点是,这一部分取过后,我们要把这个差值坐标的数量变为0,说明这几次已经取用过了,后面如果再需要这个差值坐标就不能再引用前两次的,因为前两次我们随便取后面的时候已经把后面对这两次的需要用掉了。不能重复利用。

代码

#include<iostream>
#include<vector>
#include<algorithm>
#include<map>
#include<set>
using namespace std;
#define a first
#define b seconde
#define ll long long

ll arr[100005];


signed main() 
{
    ll ans=0;
    ll n,x,y;
    cin >> n >> x >> y;
    string s;
    cin >> s;
    if (x==0 && y==0)
    {
        cout << (n*(n+1))/2 << endl;
        return 0;
    }
    ll xx=0;
    ll yy=0;
    map<ll,map<ll,ll>> M;
    M[0][0]++;
    for (int i=0;i<n;i++)
    {
        if (s[i]=='A')
        {
            xx--;
        }
        else if (s[i]=='D')
        {
            xx++;
        }
        else if (s[i]=='W')
        {
            yy++;
        }
        else
        {
            yy--;
        }
        if (M[xx-x][yy-y])
        {
            ans+=(M[xx-x][yy-y])*(n-i);
            M[xx-x][yy-y]=0;
        }
        M[xx][yy]++;
    }
    cout << ans << endl;
}
 

I Red playing cards

题意,输入一个n,然后给出2n长度的数组,里面只有1到n的数且每个只出现两次。

你可以进行任意次操作,选取一段首位数相同的区间,删掉这个区间,并获得区间长度*首位数的值的贡献,问如何操作使得值能最大。

思路:一开始一脸懵逼完全没有思路,想了半天想到一个贪心的伪解,太难实现而且大概率爆t,于是放弃,看完题解之后才知道是区间dp,确实把时间复杂度从n3优化到n2,深受启发,遂写此题解。

众所周知,dp找的是最优解,而区间dp就是从小区间到大区间一个个的区间最优解合起来得到最终的答案。那么我们不如思考,这道题给出的删除范围不就是一个个区间吗?我们直接用区间dp的思路,从小区间找最优解知道最大的区间,然后在把整体看成一个区间进行一次dp即可。

dp时还会遇到的问题就是状态转移方程,这里的转移方程就是一个问题,大区间内可能包含小区间,这个小区间是合并好还是不合并好,这是唯一的问题,也是取max更新最优的核心。具体在代码中以注释的形式细述:

代码:

#include<iostream>
#include<vector>
#include<cstdlib>
#include<algorithm>
#include<map>
#include<set>
using namespace std;
#define a first
#define b second
typedef int long long;

int cmp(pair<int, int> x, pair<int, int> y) //把段按从小到大的顺序排序
{
    return x.b - x.a < y.b - y.a;
}


signed main()
{
    int n;
    cin >> n;
    vector<pair<int, int>> pos(n + 6); //pos[i]记录值为i的左边i的位置和右边i的位置
    vector<int> arr(2*n + 5); //记录每个位置上的值
    vector<pair<int, int>> duan(n + 6); //记录每个段的开头和结尾
    for (int i = 1; i <= 2 * n; i++)
    {
        int v;
        cin >> v;
        arr[i] = v;
        if (pos[v].a) pos[v].b = i; //如果第一次出现v就是左边的v,否则就是右边的v,记录在pos里
        else
        {
            pos[v].first = i;
        }
    }
    duan = pos; //把pos赋到duan里,方便后序找对应v的位置,否则排序后找不到对应v的位置了
    sort(duan.begin() + 1, duan.begin() + n + 1, cmp); //排序
    duan[n + 1].a = 0; //注意,因为题目不一定会出现整体段,因此我们还需要最后手动加一个全部的段,这个段要在1-n范围外,就是v为0的地方,否则会多加v导致答案错误,没有这段则是没有整体整合也会导致答案错误
    duan[n + 1].b = 2 * n+1;
    vector<int> varr(2*n + 5); //varr[i]记录的是处理某一段a到b时,处理到i这个位置时候的值
    vector<int> dp(2 * n + 5); //dp[i]记录的是某一段的最优解,i是一段的最右边
    for (int i = 1; i <= n+1; i++)
    {
        int l = duan[i].a; //首先记录一下这个段的左右端点,方便最后更新和中间取用开头的值
        int r = duan[i].b;
        varr[l] = arr[l]; //这一段最左边开头的点的值的贡献肯定就是这一段左端点的值
        for (int j = l + 1; j <= r; j++) //遍历这一段的每个点
        {
            varr[j] = varr[j - 1] + arr[l]; //如果合并这个区间,就相当于把这个区间每个值都变成左端点的值,所以在上一部分基础上加左端点的值
            int le = pos[arr[j]].a;  //找到这个位置的值对应的段的左端点
            if (le != j && le > l)  //如果这个位置的值不是它那段的左端点,说明它就是那一段的右端点,且左端点在正在处理这一段的左端点右边,说明这个位置对应的b段是包含在当前处理的a段内的,而且由于我们是从小段到大段处理的,因此b段肯定是已经处理好的最优解段,那我们需要考虑的就是去掉这一段好还是保留更好
            {
                varr[j] = max(varr[j], varr[le - 1] + dp[j]); //核心,看看这一段是去掉更好,还是保留更好,保留就是取b段最优解加b段外的值,去掉就是一路加上来的值,两者取最优
            }
        }
        dp[r] = varr[r];//a段内的数据都处理完后,此时a段就是最优解了,把它保存至dp位置,为以后做准备
    }
    cout << dp[2*n+1] << endl;//最后输出合并大段的最优解即可
}

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值