牛客小白月赛121

比赛链接如下:https://ac.nowcoder.com/acm/contest/117762#question

A. Kato_Shoko

题目描述

加藤翔子现在得到一个长度为 n 的字符串 s。他希望通过删除其中的一些字符,并将剩余字符重新排列,以得到目标字符串 Kato_Shoko。
对于给定的字符串 s,请判断是否可以通过删除任意个(可为 0 个)字符,并对剩余字符进行任意重排;若能得到与目标字符串 Kato_Shoko 逐字符完全相同的字符串。如果可以,输出需要删除的最少字符数;否则输出 NO。

解题思路:用两个map统计字符的出现次数

#include<bits/stdc++.h>
using namespace std;
void solve(){
    int n; string s;
    cin>>n>>s;
    string s1="Kato_Shoko"; map<char,int> a;
    for(auto& x: s1) a[x]++;
    for(auto& x: s){
        for(auto& y: s1){
            if(x==y) { a[x]++; break; }
        }
    }
    int cnt=0;
    for(auto& x: a){
//         if(x.first=='_') {cout<<x.second<<'\n';}
//         cout<<x.second<<'\n';
        if(x.second<=1||(x.first=='o' && x.second<=3)) { cout<<"NO"<<'\n'; return; } 
        cnt+=x.second;
        
    }
    cnt-=s1.size();
//     cout<<cnt<<'\n';
    cout<<"YES"<<" "<<n-s1.size()<<'\n';
    //总字符数:n 包含目标字符数: cnt 包含但是多余的字符:cnt-s1.size() 不包含的:n-cnt
    //删除的字符:n-cnt+cnt-s1.size()
}
int main(){
    int t=1;
    while(t--){
        solve();
    }
}
#include<bits/stdc++.h>
using namespace std;
void solve(){
    int n;
    cin>>n;
    string s;
    cin>>s;
    string res="Kato_Shoko";
    unordered_map<char,int>mp1,mp2;
    for(auto x:s){
        mp1[x]++;
    }
    for(auto x:res){
        mp2[x]++;
    }
    for(auto x:mp2){
        char c=x.first;
        int r=x.second;
        if(mp1[c]<r){
            cout<<"NO"<<endl;
            return;
        }
    }
    cout<<"YES"<<' '<<n-res.length()<<endl;
}
int main(){
    int t=1;
    while(t--){
        solve();
    }
}

B. 白绝大军

运营可以投入“白绝大军”机器人来补足全服活跃。每个机器人会完整复制某个真实玩家的活跃量(即选择玩家 i,该机器人贡献值为 ai​)。可以多次复制同一玩家,但每位玩家最多被复制 bib_ibi​ 次。机器人数等于所有复制次数的总和。
现知道 n 名真实玩家的活跃 a1,…,an 与对应的复制上限 b1,…,bn​ 以及目标活跃度 t。在尽可能少使用机器人的前提下,使得总活跃(真实玩家之和 + 机器人贡献之和)至少达到 t。若已满足输出 0,若无论如何都无法达到输出 −1。

解题思路:优先选活跃度高的玩家进行复制

#include<bits/stdc++.h>
using namespace std;
using pci = pair<char, int>;
using ll=long long;
void solve() {
    ll n,t;cin>>n>>t;
    vector<ll> a(n); ll tot=0;
    for(int i=0;i<n;i++) { cin>>a[i]; tot+=a[i];}
    if(tot>=t){
        cout<<0<<'\n'; return;
    }
    vector<ll> d;
    for(int i=0;i<n;i++){
        int b; cin>>b;
        while(b--){
            d.push_back(a[i]);
        }
    } 
    sort(d.begin(),d.end(),[](auto& x,auto& y){
        return x>y; 
    });
    ll ans=0;
    for(auto& x: d){
        tot+=x;
        ans++;
        if(tot>=t) { cout<<ans<<'\n'; return;}
    }
    cout<<-1<<'\n';
}
int main() {
    int t = 1;
    while (t--) {
        solve();
    }
    return 0;
}
// >=t

C.重组猎魔团试炼

给定一串承载魔力的长度为 n 的数字符文 s,长度代表猎魔团人数,以及古老的整除咒语量值 d。龙浩晨必须从 s 中至少选择一个符文,且每个符文最多被选择一次,将所选的符文任意重排组成一个新的法阵编号(允许前导零),并施放整除咒语:只有当这个新的法阵编号能被 d 整除,试炼才算通过。
翔子需要帮助龙浩晨团队找出能够通过整除咒语的最小法阵编号;若无法通过试炼,则重组失败。
注意,最小法阵编号按十进制数值比较,取最小者;输出为该整数的十进制表示,不保留多余前导零(若答案为 0,输出 0)。

解题思路:暴力枚举所有可能的非空子集

#include<bits/stdc++.h>
using namespace std;
using pci = pair<char, int>;
using ll=long long;
void solve() {
    int n,d; 
    cin>>n>>d;
    string s; cin>>s;
    vector<int> a(n);
    for(int i=0;i<n;i++){ a[i]=s[i]-'0'; } ll ans=LLONG_MAX;
    for(int m=1;m<(1<<n);m++){
        vector<int> x;
        for(int i=0;i<n;i++){
            if((m>>i)&1){
                x.push_back(a[i]);
            }
        }
        sort(x.begin(),x.end()); 
        do{
            ll res=0;
            for(auto& v: x){
                res=res*10+v;
            }
            if(res%d==0) ans=min(ans,res);
        }while(next_permutation(x.begin(),x.end()));
    }
    if(ans==LLONG_MAX) cout<<-1<<'\n'; 
    else cout<<ans<<'\n';
}
int main() {
    int t = 1;
    while (t--) {
        solve();
    }
    return 0;
}
// 从s中选一个非空子集

D. 谁敢动资本的蛋糕

现有 n 种美食,它们各自被赋予一个非负整数,代表资本对它们的关注度:a={a1,a2,…,an}。翔子会将这些食物全部带走,但是翔子只有一个袋子,也就是说她只能带走一个食物,于是她必须将这些美食两两融合。
她会将所有的食物两两融合,经过恰好 n−1 次操作,最终只留下一个“终极美食”。每一次融合,都会引发资本的注意,代价如下:
,∙任选数组中的两个美食 (x,y),将它们从数组中删除,并将它们关注度的异或结果 ax xor ⁡ay​ 插入数组,同时支付“合并成本” cost(x,y)=2×(ax and ⁡ay)。
恰好进行 n−1 次操作后,数组中只剩下一个“终极美食”。
请你计算并输出,让总合并成本最小化时的最小总成本。

解题思路:每次合并数组值都会减少2*(ax & ay),所以和并的总成本为初始数组总和减去合并后的数组总和

#include<bits/stdc++.h>
using namespace std;
using pci = pair<char, int>;
using ll=long long;
void solve() {
    int n; cin>>n;
    vector<ll> a(n); ll sum=0; ll x =0;
    for(int i=0;i<n;i++){
        cin>>a[i]; sum+=a[i]; x^=a[i];
    }
    cout<<sum-x<<'\n';
    
}
int main() {
    int t = 1;
    while (t--) {
        solve();
    }
    return 0;
}
// ax XOR ay cost=2*(ax & ay)
//az ax XOR ay
//az XOR ax XOR ay
//cost1=2*(az & ax XOR ay)
//2+3=2 xor 3 + 2 * (2&3)
//2 * (2&3) = 2 + 3 - 2 xor 3

E. 构造序列

题目描述

加藤翔子想构造一个神秘的序列,所以她给定了正整数 n 和 m。加藤翔子考虑所有长度为 n 的整数序列 a={a1,a2,…,an},其中每个 ai 的取值为 1,2,…,m。
将序列划分为若干个连续的段,使得每个段内的所有元素都相等,且任意相邻两个段的元素都不相等。我们称这样划分出的段为 R 段。设这些 R 段的长度依次为 l1,l2,…,lk​。若 l1<l2<⋯<lk​,则称该序列为「合法」。
现在她想要考考你,请你回答「合法」序列的个数是多少?由于答案可能很大,请将答案对 (10^9+7) 取模后输出。

解题思路:

题目理解
我们有一个长度为 n 的序列,每个位置取值 1…m。

合法条件:

把序列按“连续相同元素”分段。

相邻段的元素值必须不同。

每个段的长度(该段元素个数)必须严格递增。

例如:
1 1 2 2 2 3 3 3 3
分段:[1 1](长度 2)、[2 2 2](长度 3)、[3 3 3 3](长度 4)
长度序列:2, 3, 4 → 严格递增 → 合法。

设一共有 k 个 R 段,它们的长度分别是 L1<L2<...<Lk

L1+L2+...+Lk=n

所以第一步是:
把 n 拆分成 k 个互不相同的正整数,且按递增排列(其实严格递增就是互不相同,且顺序固定为从小到大)

这种拆分的方案数记为 P(n,k)。

因此, 一旦我们确定了这 k 个长度值,它们在序列中出现的顺序就是固定的:最短的段在最前面,最长的段在最后面、、

所以问题变成:

1.先枚举 k(段数)。

2.对每个 k,计算 P(n,k)。

3.对每个这样的长度划分方案,计算有多少种给段赋值的方案。

赋值的方案数
第一段:可以取 m 种值。

第二段:必须与第一段不同 → m−1 种。

第三段:必须与第二段不同 → m−1 种。

……

第 k 段:必须与第 k−1 段不同 → m−1 种。

所以总的赋值方案数:

m⋅(m−1)^k−1

对每个可能的 k 求和:

ans = k= 1..K { P(n,k) * m *  (m-1)^k-1 }

其中 K 是最大可能的段数,由最小长度和决定:
最小的 k 个互不相同正整数的和是 1+2+⋯+k=k(k+1)/2,必须满足 k(k+1)/2≤n

K=根号2n

如何计算 P(n,k) ?

我们要计算 dp[i][j]:把整数 i 拆分成 j 个严格递增的正整数(即互不相等且从小到大排列)的方案数。

例如:i=8,j=3, 一种拆法是 1+3+4=8

分类讨论
我们按拆分中最小的值 L1, 分成两种情况:

情况1:

最小项等于 1

拆分形式:

L1=1<L2<...<Lj , sum(j)=i

去掉第一个数 1,剩下 j−1 个数:

L1<L3<...<Lj

它们的和是 i−1

注意:因为原来严格递增且 L1=1ℓ,所以 L2≥2

现在对剩下的每个数减去 1:

L2−1, L3−1, …,Lj−1

这些数仍然是严格递增的正整数(因为 L2​−1≥1,且相邻差不变)

项数:j−1

总和:

(L2+⋯+Lj)−(j−1)=(i−1)−(j−1)=i−j

所以,情况 1 的拆分 与 把 i−j 拆成 j−1 个严格递增正整数 是一一对应的。

因此,情况 1 的方案数 = dp[i−j][j−1]

情况2:

最小项至少为 2

拆分形式:

L1≥2, L1<L2<⋯<Lj, sum(Lj)=i

对每个数都减去 1:

Lt=(Li  - 1 )>=1 (t=1,...,j)

这些Lt仍然是严格递增的正整数, 项数还是 j

所以,情况 2 的拆分 与 把 i−j 拆成 j 个严格递增正整数 是一一对应的。

因此,情况 2 的方案数 = dp[i−j][j]。

 

两种情况互不重叠(最小项要么等于 1,要么 ≥2),且覆盖了所有可能。

所以: dp[i][j]=dp[i−j][j−1]+dp[i−j][j]

DP边界:

​​​​​​dp[0][0]=1(空拆分,0 分成 0 个部分算 1 种方案)。

如果 j=1,那么 dp[i][1]=1(只有一种拆分:i 本身)。

 

#include<bits/stdc++.h>
using namespace std;
using pci = pair<char, int>;
using ll=long long;
const int M=1e9+7;
const int N=1e5+10;
const int K=500;
ll dp[N][K];
ll fun(ll a , ll b){
    ll res = 1;
    while (b) {
        if (b & 1) res = res * a % M;
        a = a * a % M;
        b >>= 1;
    }
    return res;
}
void solve() {
    int n,m;
    cin>>n>>m;
    dp[0][0]=1;
    for(int i=1;i<=n;i++){
        for(int j=1;j<K;j++){
            if(i<j) continue;
            dp[i][j]=(dp[i-j][j-1]+dp[i-j][j])%M;
        }
    }
    ll ans=0;
    for(int k=1;k<K;k++){
        if(k*(k+1)/2>n) break;
        if(dp[n][k]==0) continue;
        ll x = m % M * fun((m - 1) % M, k - 1) % M;
        ans = (ans + dp[n][k] * x) % M;
    }
    cout << ans << '\n';
}
int main() {
    int t = 1;
    while (t--) {
        solve();
    }
    return 0;
}
// 按1,2,3...进行划分
// n的最大值为10^5 最多段数划分是:1+2+3+...+n=10^5
// n=500
// 将长度为n的序列,拆分成k个互不相同的数字

F. 区间或与与再异或之和最大值

​​​​​​https://ac.nowcoder.com/acm/contest/117762/F

题面全是图片, 贴不了
现在加藤翔子又得到长度为 n 的整数序列 a1,a2,…,an,他需要将序列划分成若干个不相交的连续段(子区间),使得所有子区间的「价值」之和最大。

解题思路:对于分出的每个子区间/字段的价值为:子区间内的所有数按位与, 按位或,最后再进行一次按位异或

固定右端点 i,从 j=i−1 向左扩展,区间 OR 值 O(j+1,i) 单调不减、AND 值 A(j+1,i) 单调不增,
每种 (O,A) 组合在向左扩时只会变化 ≤30+30 次,总共 O(60) 个不同区间块。

用一个结构体 Block 存储每段相同值块:其中该块覆盖所有 j+1∈[L..R]。

定义 dp[i] 为前 i 个元素的最优值,转移:

dp[i]  表示 [0,i] 这一段总价值的最大值

dp[i]=max   j=1...i-1 { dp[j] +  val(j+1.. i) }

val(j+1.. i) 表示从j+1 到 i  这一段的价值

为高效取区间最大值,引入线段树支持 O(log⁡N) 的点更新与区间最大查询。

#include <bits/stdc++.h>
#define il inline

using namespace std;
using ll = long long;
using ull = unsigned long long;
using int128=__int128_t;

const ll N = 2e5 + 5, mod = 998244353, inf = 1e18;
const double esp=1e-3;
double PI=3.1415926;

ll tree[N<<2];
il int lson(int i){
    return i<<1;
}
il int rson(int i){
    return i<<1|1;
}

il void up(int i){
    tree[i]=max(tree[lson(i)],tree[rson(i)]);
}

il void updata(int i,int pl,int pr,int L,int R,ll val){
    if(L<=pl&&pr<=R){
        tree[i]=val;
        return ;
    }
    int mid=pl+pr>>1;
    if(L<=mid){
        updata(lson(i),pl,mid,L,R,val);
    }else{
        updata(rson(i),mid+1,pr,L,R,val);
    }
    
    up(i);
}

il ll query(int i,int pl,int pr,int L,int R){
    if(L<=pl&&pr<=R){
        return tree[i];
    }
    ll ans=-inf;
    int mid=pl+pr>>1;
    if(L<=mid){
        ans=max(ans,query(lson(i),pl,mid,L,R));
    }
    if(R>mid){
        ans=max(ans,query(rson(i),mid+1,pr,L,R));
    }
    return ans;
}

il void solve(){
    int n;
    cin>>n;
    vector<ll>a(n+1),dp(n+1,-inf);
    //dp[i]:前i个元素的最优值
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    dp[0]=0;
    //下标从1开始,但是1点存的是dp[0],2点存的是dp[1]...
    updata(1,1,n+1,1,1,dp[0]);
    struct Block {
        ll or_v, and_v;
        int L, R;
    };
    vector<Block>blocks;   // 上一轮的块列表
    vector<Block>nxt;      // 用于构建新一轮的块

    for(int i=1;i<=n;i++){
        nxt.clear();
        //新区间[i,i]
        nxt.push_back({a[i], a[i], i, 0});

        //拓展旧块
        for(auto &[or_v,and_v,L,R]:blocks){
            ll new_or=or_v|a[i],new_and=and_v&a[i];

            if(nxt.back().or_v==new_or&&nxt.back().and_v==new_and){
                //合并:只要更新最小的 L
                nxt.back().L = min(nxt.back().L, L);
            }else{
                //新开一个块
                nxt.push_back({new_or,new_and,L,0});
            }
        }

        //填充R,按下标0..sz-1顺序,块0的R=i,之后R=前一个块的L-1
        int sz=nxt.size();
        for(int k=0;k<sz;k++){
            nxt[k].R=(k==0?i:nxt[k-1].L-1);
        }

        ll res=-inf;
        for(auto [o,a,L,R]:nxt){
            ll val=o^a;
            //因为线段树偏移,所以查询区间刚好是L,R
            ll max_dp=query(1,1,n+1,L,R);
            res=max(res,max_dp+val);
        }
        dp[i]=res;
        //开点
        updata(1,1,n+1,i+1,i+1,dp[i]);
        blocks.swap(nxt);
    }
    cout<<dp[n];
}

int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    //cin >> t;
    while (t--) {
        solve();
    }
    return 0;
}

评论区的另一种思路:

考虑 DP + 从左往右扫描。

首先分别探讨二进制下的每一位,一段区间的值的为1当且仅当这段区间的既有0也有1,于是可以直接计算每一位的前缀和,在 O(log⁡ wmax)时间内计算出区间 [l,r] 的答案。

然后 dp[i] 的值随 i 减小而减小(不增),从位置 i 向左找到最大的 L ,使得区间 [L,i] 内的所有数字的第 j 位上至少有一个 1 和一个 0 (亦即找到最大的 L(L<i) 使得 a[L] 的第 j 位跟 a[i]第 j 位不同),可以开一个数组 last[N][log⁡wmax] 记录每个数字的每一位的对应的 L 。从位置 ii 最多会向左跳 log⁡wmax​ 次。 last 数组的构造方式请看代码。

转移方程为: dp[i]=max(dp[i],dp[L−1]+val(L,i))(其中 L=last[i][j] j∈[0,log⁡wmax]

为什么只需要跳这么点次数?

这里只解释一位的情况:

1.假设有一个 0/1 数组 a=[1,1,0,0,1,1,0,0,0,0],长度为 10 。当 i=10 时,显然的 L=6 ,按照上述转移方程, dp[i] 将从 dp[L−1]+val(L,i) 更新,如果从 dp[L]+val(L+1,i) 更新,那么 dp[L] 不一定会比 dp[L−1] 多 11 ,但是 val(L,i) 一定比 val(L+1,i)多 1 ,选择 dp[L−1]+val(L,i) 一定是不劣的,另外因为区间 [7,9] 全都是 0 ,这个区间里的 dp[j] 绝对不会比 dp[L] 大。

2. 肯定不选更左的 j 来更新 dp[i] ,因为往左走 dp[j−1] 可能会减小,但是 val(j,i)一定不会再增加了。

#include <bits/stdc++.h>
using namespace std;
using LL = long long;

constexpr int N = 200050;
int n;
int a[N];
int last[N][30][2];
int pre[N][30];
LL dp[N];

LL get(int l, int r) {
  int ret = 0;
  for (int j = 0; j < 30; ++j) {
    int cur = pre[r][j] - pre[l - 1][j];
    if (cur != r - l + 1 && cur != 0) {
      ret |= (1 << j);
    }
  }
  return ret;
}

int main() {
  std::cin.tie(nullptr)->sync_with_stdio(false);
  
  cin >> n;
  for (int i = 1; i <= n; ++i) {
    cin >> a[i];
    for (int j = 0; j < 30; ++j) {
      last[i][j][0] = last[i - 1][j][0];
      last[i][j][1] = last[i - 1][j][1];
      if (i > 1) {
        if (a[i - 1] >> j & 1) {
          last[i][j][1] = i - 1;
        } else {
          last[i][j][0] = i - 1;
        }
      }
    }
    for (int j = 0; j < 30; ++j) {
      pre[i][j] = pre[i - 1][j] + (a[i] >> j & 1);
    }
    for (int j = 0; j < 30; ++j) {
      int l = last[i][j][(a[i] >> j & 1) ^ 1];
      if (l) {
        dp[i] = max(dp[i], dp[l - 1] + get(l, i));
      }
    }
  }

  cout << dp[n];
  
  
  return 0;
}

这星期的牛客周赛就不更了....

感谢大家的点赞和关注,你们的支持是我创作的动力!

 

### 关于小白109的信息 目前并未找到关于小白109的具体比信息或题解内容[^5]。然而,可以推测该事可能属于网举办的系列算法之一,通常这类比会涉及数据结构动态规划、图论等经典算法问题。 如果要准备类似的事,可以通过分析其他场次的比题目来提升自己的能力。例如,在小白13中,有一道与二叉树相关的题目,其核心在于处理树的操作以及统计最终的结果[^3]。通过研究此类问题的解决方法,能够帮助理解如何高效地设计算法并优化时间复杂度。 以下是基于已有经验的一个通用解决方案框架用于应对类似场景下的批量更新操作: ```python class TreeNode: def __init__(self, id): self.id = id self.weight = 0 self.children = [] def build_tree(n): nodes = [TreeNode(i) for i in range(1, n + 1)] for node in nodes: if 2 * node.id <= n: node.children.append(nodes[2 * node.id - 1]) if 2 * node.id + 1 <= n: node.children.append(nodes[2 * node.id]) return nodes[0] def apply_operations(root, operations, m): from collections import defaultdict counts = defaultdict(int) def update_subtree(node, delta): stack = [node] while stack: current = stack.pop() current.weight += delta counts[current.weight] += 1 for child in current.children: stack.append(child) def exclude_subtree(node, total_nodes, delta): nonlocal root stack = [(root, False)] # (current_node, visited) subtree_size = set() while stack: current, visited = stack.pop() if not visited and current != node: stack.append((current, True)) for child in current.children: stack.append((child, False)) elif visited or current == node: if current != node: subtree_size.add(current.id) all_ids = {i for i in range(1, total_nodes + 1)} outside_ids = all_ids.difference(subtree_size.union({node.id})) for idx in outside_ids: nodes[idx].weight += delta counts[nodes[idx].weight] += 1 global nodes nodes = {} queue = [root] while queue: curr = queue.pop(0) nodes[curr.id] = curr for c in curr.children: queue.append(c) for operation in operations: op_type, x = operation.split(' ') x = int(x) target_node = nodes.get(x, None) if not target_node: continue if op_type == '1': update_subtree(target_node, 1) elif op_type == '2' and target_node is not None: exclude_subtree(target_node, n, 1) elif op_type == '3': path_to_root = [] temp = target_node while temp: path_to_root.append(temp) if temp.id % 2 == 0: parent_id = temp.id // 2 else: parent_id = (temp.id - 1) // 2 if parent_id >= 1: temp = nodes[parent_id] else: break for p in path_to_root: p.weight += 1 counts[p.weight] += 1 elif op_type == '4': pass # Implement similarly to other cases. result = [counts[i] for i in range(m + 1)] return result ``` 上述代码片段展示了针对特定类型的树形结构及其操作的一种实现方式。尽管它并非直接对应小白109中的具体题目,但它提供了一个可借鉴的设计思路。 ####
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值