牛客小白月赛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≤nK=根号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(logN) 的点更新与区间最大查询。
#include <bits/stdc++.h>
#define il inlineusing 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){//合并:只要更新最小的 Lnxt.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-1int 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,Rll 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][logwmax] 记录每个数字的每一位的对应的 L 。从位置 ii 最多会向左跳 logwmax 次。 last 数组的构造方式请看代码。
转移方程为: dp[i]=max(dp[i],dp[L−1]+val(L,i))(其中 L=last[i][j] j∈[0,logwmax]
为什么只需要跳这么点次数?
这里只解释一位的情况:
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;
}
这星期的牛客周赛就不更了....
感谢大家的点赞和关注,你们的支持是我创作的动力!