比赛链接如下: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 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][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;
}
这星期的牛客周赛就不更了....
感谢大家的点赞和关注,你们的支持是我创作的动力!
937

被折叠的 条评论
为什么被折叠?



