有问题请评论区聊喵!!!

A.Kato_Shoko

问题回顾

给定长度为 )的字符串
中选取一些字符,对其进行任意重排 求能否组成目标字符串 Kato_Shoko
如果可以,输出 YES 以及需要删除的最少字符数
若无解输出 NO

核心思路

最终字符串要变成 Kato_Shoko,也就是说最终字符串长度只能是

  1. 统计字符数量
  2. 如果有字符串 Kato_Shoko 对应的数量的字符,那么就可以,否则就输出 NO
  3. 输出 YES 以及

代码展示

#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 = 1e6 + 5, mod = 1e9+7, inf = 2e18;
const double esp=1e-9;
double PI=3.1415926;

il void solve(){
    ll n;
    string s;
    cin>>n>>s;
    map<char,int>mp;
    for(auto ch:s)mp[ch]++;
    string t="Kato_Shoko";
    for(auto ch:t){
        if(--mp[ch]<0){
            cout<<"NO";
            return ;
        }
    }
    cout<<"YES "<<n-10;
}

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

B.白绝大军

题解

核心思路

贪心最优解是:每次选取当前最大的可用项,即把按降序排序,依次累加总活跃度直到达到或超过 。理由直观:若希望用最少的项达到一定和,必然优先使用大的项(换位交换证明也很容易写出)。

证明

要最小化选取项数使得和 ,任何最优解中若存在两项 ,而我们选了 但没有选 (且 可选),将 换为 能使总和增加或不变,项数不变,因此不会变差。

总结

关键在于把每个 次展开为一个“可选项”集合,然后按降序贪心取数直到满足目标。(特别是 )。

代码展示

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

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

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

il void solve(){
    ll n,t,sum=0;
    cin>>n>>t;
    vector<ll>a(n+1);
    for(int i=1;i<=n;i++){
        cin>>a[i];
        sum+=a[i];
    }

    if(sum>=t){
        cout<<0;
        return ;
    }

    vector<ll>all;
    for(int i=1;i<=n;i++){
        ll b;cin>>b;
        while(b--){
            all.push_back(a[i]);
        }
    }
    sort(all.rbegin(),all.rend());
    ll ans=0;
    for(auto v:all){
        sum+=v;
        ans++;
        if(sum>=t){
            cout<<ans;
            return ;
        }
    }
    cout<<-1;
}

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


C.重组猎魔团

问题回顾

给定长度为 )的符文字符串 和咒语力量值 )。
中选取一个非空子集,对其中数字进行任意重排(允许前导零)得到一个整数,要求该整数能被 整除。
求所有可行结果中的最小值,若无解输出

核心思路

由于 ,可以直接枚举:

  1. 子集枚举:用位掩码 mask,筛选数字。
  2. 排列枚举:对当前子集排序后,使用 next_permutation 枚举所有排列。
  3. 验证整除:将排列拼成整数 res,若 res % D == 0,则更新最小值。

代码展示

#include <bits/stdc++.h>

using namespace std;
using ll = long long;
const ll N = 1e5 + 5, mod = 998244353, inf = 1e18;

void solve(){
    int n,d;
    cin>>n>>d;
    string s;
    cin>>s;
    vector<int>a;
    for(auto c:s){
        a.push_back(c-'0');
    }
    ll ans=inf;
    for(ll mask=1;mask<(1ll<<n);mask++){
        vector<int>num;
        for(int i=0;i<n;i++){
            if((mask>>i)&1){
                num.push_back(a[i]);
            }
        }
        sort(num.begin(),num.end());
        do{
            ll res=0;
            for(auto v:num)res=res*10+v;
            if(res%d==0)ans=min(ans,res);
        }while(next_permutation(num.begin(),num.end()));
    }
    cout<<(ans==inf?-1:ans);
}

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

    }
    return 0;
}

D.谁敢动资本的蛋糕

问题分析

给定一个长度为 的数组 。每次可以选取两个数 ,将它们删除并插入 ,同时支付代价经过 次操作后,数组归约为单个数,要求计算出总代价的最小值。

解题思路

这个题有很多种解法,我说一下出这个题的时候的解法,

方法一

利用恒等式

  • 对一次合并(把两个数 合并为 )的代价定义为

  • 合并后数组中元素和 会减少该

  • 反复合并直到只剩下一个数时,最终留下的数恰好是整体异或

  • 因此所有合并造成的总成本为初始数组和与最终剩余值之差:

    并且该值与合并顺序无关。

#include <bits/stdc++.h>

using namespace std;
using ll = long long;

void solve(){
    ll n;
    cin>>n;
    ll sum=0,XOR=0;
    while(n--){
        ll a;cin>>a;
        sum+=a;
        XOR^=a;
    }
    cout<<sum-XOR;
}
int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);   
    int t = 1;
    //cin >> t;
    while (t--) {
        solve();
    }
    return 0;
}

方法二

一开始想利用利用恒等式来出这个题的,出完后过了几天突然想到这个丑陋的解法了,那就是利用二进制位独立性的特性:

对每个二进制位 ),统计数组中该位为 1 的元素个数

在第 位上,每合并一对在该位均为 1 的数,会消除两个 1,成本为

最多可以配对合并 次,因此第 位的最小总成本为

将所有位的成本累加,即得到全局最优成本。

整体时间复杂度为 ,其中 ,空间复杂度为

实现步骤

  1. 读入整数 n
  2. 初始化长度 61 的计数数组 cnt,初值为 0;
  3. 对数组中每个元素 x,遍历位 ,若当前位的二进制位 1,则 cnt[b]++
  4. 对每个位 ,令 pairs = cnt[b]/2,将 ans += pairs * (1LL<<b)
  5. 输出 ans*2

代码展示

#include <bits/stdc++.h>

using namespace std;
using ll = long long;

void solve(){
    int n;
    cin>>n;
    vector<ll>cont(70);
    for(int i=1;i<=n;i++){
        ll x;
        cin>>x;
        for(int mask=0;mask<=60;mask++){
            if((x>>mask)&1){
                cont[mask]++;
            }
        }
    }
    ll ans=0;
    for(int mask=0;mask<=60;mask++){
        ans+=(1ll<<mask)*(cont[mask]>>1);
    }
    cout<<(ans<<1)<<'\n';
}

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

E.构造序列

思路

把一个合法序列看作有 个 R,R 长度为 正整数且严格递增:

因此问题可拆成两部分:

  1. 计数有多少种严格递增的长度序列 (即把 分成 个互不相等的正整数且按升序排列)。设该计数为
  2. 对于固定的某个长度序列,取值有多少选择?第一个 R 有 种选择,之后每个 R 必须与前一 R 不同,因此总数为

把两部分相乘并对所有合法的 求和即可:

其中只考虑满足最小和约束的

的计算

OI-Wiki的相关章节-互异分拆数亦有记载

我们用 DP 计数把整数 划分为 严格递增 的正整数的方案数,记

初始 ,其余为

递推:

递推的证明

我们把所有把 拆成 个严格递增正整数的方案,按照最小项的值分成两类来计数。

情况 A:最小项等于 .
假设某一拆分为

去掉这一个 ,剩下 个部分 。由于原序列严格递增且 ,对剩余的每个部分减去 得到

这是一组严格递增的正整数,共 项,且其和为

因此,“最小项为 ” 的所有拆分与把 拆成 个严格递增正整数的一一对应,方案数为

情况 B:最小项至少为 .
如果拆分中每一项都 ,令 (对所有 )。则每个 ,且仍保持严格递增,项数为 ,并且和为

所以“最小项 ” 的拆分集合与把 拆成 个严格递增正整数的一一对应,方案数为

合并。 由于两类互不相交并覆盖所有可能,故

从而得到所述递推。

边界注意。。若最小可能和 也可直接剪枝为 0。

复杂度

  • DP 的规模为 ,其中 为满足 的最大
    ,总体复杂度

代码展示

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

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

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

il ll quikc_mi(ll a,ll b){
    ll ans=1;
    while(b){
        if(b&1){
            ans=ans*a%mod;
        }
        b>>=1;
        a=a*a%mod;
    }
    return ans;
}

il void solve(){
    ll n,m;
    cin>>n>>m;
    const ll J=450;
    vector<vector<ll>>dp(n+1,vector<ll>(J+1));
    dp[0][0]=1;
    for(ll i=1;i<=n;i++){
        for(ll j=1;j<=min(i,J);j++){
            dp[i][j]=(dp[i-j][j-1]+dp[i-j][j])%mod;
        }
    }

    ll ans=0;
    for(ll k=1;k<=J;k++){
        if(n<k*(k+1)/2)break;
        ll p=quikc_mi(m-1,k-1);
        ll res=dp[n][k];
        res=res*m%mod*p%mod;
        ans=(ans+res)%mod;
    }
    cout<<ans;
}

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


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

问题回顾

给定长度为 的正整数序列 ,需要将序列划分成若干连续子区间,最大化(原题式子在此省略,可按题面补充)

其中 ,

核心思路

固定右端点 ,从 向左扩展,区间 OR 值 单调不减、AND 值 单调不增,
每种 组合在向左扩时只会变化 次,总共 个不同区间块。

用一个结构体 Block 存储每段相同值块:其中该块覆盖所有

定义 为前 个元素的最优值,转移:

(原转移公式请按题面补充)

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

整体复杂度

算法流程

  1. 初始化:令 ,构造大小为 的线段树 st,并执行 st.update(0,0)
  2. 对每个
    1. 构造新块列表 nxt
      • 首先插入单元素块
      • 遍历旧块 ,计算新值 ,并将相同 的块合并。
    2. 确定每块的右端: 对第 块设置
    3. DP 转移:对每块
      查询
      并更新
    4. 更新线段树:st.update(i, dp[i]),然后将 blocks 替换为 nxt
  3. 最终输出

复杂度分析

每个 生成 个块,
每个块做一次 的线段树查询,
更新一次点值。
总计 ,满足

代码展示

#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;
}

最后有什么疑问可以在评论区指出来喵!!!