- ACM
2024新生赛题解
- 2025-7-31 13:34:32 @
Written by @Yue_chen
=================================================================================================================
镇楼
Update 12.3 11:13
A:再见603(贪心)
题意:
给n个字符串,可以任意两两拼接无限次,最多能拼多少个“603”
思路:
由样例可知,603串必须仅包含603串,如6031、9603等都是不合法的;
因此,记录603的所有子串“603”,“60”,“03”,“6”,“0”,“3”;
然后进行贪心,优先选择用最长的串:
ans += cnt("603");
ans += min(cnt("60"), cnt("3"));
ans += min(cnt("6"), cnt("03"));
ans += min({cnt("6"), cnt("0"), cnt("03")});
计算的时候记得将已经统计的串去除,cnt为该串出现的次数,可以使用map,哈希等方式保存。
时间复杂度O(nlog2****n)
AC代码:
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
map<string, int> mp;
int n; cin>>n;
for(int i=1; i<=n; ++i) {
string s; cin>>s;
mp[s]++;
}
i64 ans = 0;
ans += mp["603"];
int mn = min(mp["60"], mp["3"]);
ans += mn;
mp["60"] -= mn;
mp["3"] -= mn;
mn = min(mp["6"], mp["03"]);
ans += mn;
mp["6"] -= mn;
mp["03"] -= mn;
mn = min({mp["6"], mp["0"], mp["3"]});
ans += mn;
cout<<ans<<"\n";
}
signed main(void) {
ios::sync_with_stdio(false); cin.tie(nullptr);
int t=1;// cin>>t;
while (t--) solve();
return 0;
}
B:欢迎加入实验室(构造+贪心)
题意:
给定一个长度 n 和一个 k ;
我们必须构造一个长度为 n 的序列 a(序列的定义为 1~n 当且仅当出现一次),并且,该序列 |a[i] - i| >= k。
并且使字典序尽可能小。
思路:
答案唯一,一眼贪心。
当 k > n/2 时:
显然数组中间的数无论如何也没法满足|a[i] - i| >= k,直接输出-1。
当 k < n/2 时:
我们根据贪心发现,当 n >= 4*k时,(n, k)可以转移为(n - k*2, k);
例如:
9 2 5 2
3 4 1 2 7 8 9 5 6 -> 7 8 9 5 6
由于前2*k位满足贪心的最小字典序,k很小时,不论 n 多大时,前缀数组一定为 3 4 1 2。因此我们可以直接推出前缀固定的排列方式。
在上一步的化简之后,n 总会 < 4*k:
我们根据贪心方法手玩一下四个样例:
8 3
4 5 6 7 8 1 2 3
9 3
4 5 6 7 8 9 1 2 3
10 3
4 5 6 1 8 9 10 2 3 7
11 3
4 5 6 1 2 9 10 11 3 7 8
我们发现 当 n <= 3*k时:
将 [1 2 3 ... n-1 n] 循环左移k次,就是最终答案
当 3*k < n < 4*k 时:
出现了一种特殊的构造方式,我们尝试模拟一下这种构造方式;
实际上是先把最后k个数前移k次,再把前k个数后移k次,如果位置撞了,就顺延到后面的位置上,直到找到空位为止。
剩下的数依次从小到大找到一个空的位置。
时间复杂度O(n)
AC代码:
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
int n, k; cin>>n>>k;
vector<int> p(n+1);//答案数组
int m = n/(2 * k);
//初步判断
if(m == 0) {
cout<<"-1\n";
return ;
}
//化简转移
int j = 1;
while(m > 1) {
for(int i=j; i<j+k; ++i) {
p[i] = i + k;
}
for(int i=j+k; i<j+2*k; ++i) {
p[i] = i - k;
}
m--;
j += 2*k;
}
//分讨构造
int len = n-j+1;
if(len > k*3) {
//先放后k个
for(int i=n-k,x=0; i>n-k-k; --i,x++) {
p[i] = n-x;
}
//再放前k个
for(int i=j+k,x=0; x<k and i<=n; i++) {
if(!p[i]) {
p[i] = j+x; x++;
}
}
//最后总小到大依次放
for(int i=j,x=j+k; i<=n; ++i) {
if(!p[i]) {
p[i] = x++;
}
}
}else {
//直接循环左移k次放
for(int i=j; i<=n-k; ++i) {
p[i] = i + k;
}
for(int i=n-k+1; i<=n; ++i) {
p[i] = j + i - (n-k+1);
}
}
for(int i=1; i<=n; ++i) {
cout<<p[i]<<" \n"[i==n];
}
}
signed main(void) {
ios::sync_with_stdio(false); cin.tie(nullptr);
int t=1; //cin>>t;
while (t--) solve();
return 0;
}
C:奶龙日历(数学)
题意:
由题意化简得出式子:
(x-y)*(d-1) % w == 0;d, w已经给出,x,y的上界也分别给出。
求满足上式的二元组(x, y)的数量。
思路:
式子化简由题意给出的两个式子相减得到,不再赘述。
由于y > x,设T = y-x,T的范围为[1, min(m, d)-1],得到:
T * (d - 1) % w == 0
设 g = gcd(d-1, w),将w/g,此时d-1 与 (w/g)互质,直接消掉,得到:
T % (w/g) == 0
我们发现T的数量就是(w/g)的所有倍数。
由于我们有了T的范围,所以可以直接用 T/ (w/g) 得到T的数量cnt[T]。
对于每一个T,他的贡献为 min(m, d) - T;
可以发现,随着T的增大,cnt的值是一个等差数列。
因此直接使用等差数列求和公式解决该问题。
最终答案ans = min(m,d) * cnt[T] - (cnt[T] * (cnt[T] -1) / 2 * T);
时间复杂度O(1)
AC代码:
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
constexpr i64 M = 1e9;
void solve() {
i64 m, d, w; cin>>m>>d>>w;
i64 k = w / gcd(d-1, w);
i64 q = min(m, d) / k;
cout<<(min(m, d) * q - (q + 1) * q / 2 * k)<<"\n";
}
signed main(void) {
ios::sync_with_stdio(false); cin.tie(nullptr);
int t=1; cin>>t;
while (t--) solve();
return 0;
}
D:计计......计算几何???(签到)
题意:
输出三数相乘。
思路:
注意输入有小数,并且输出保留两位
时间复杂度O(1)
AC代码:
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
long double x, y, z; cin>>x>>y>>z;
cout<<fixed<<setprecision(2)<<(x*y*z)<<"\n";
}
signed main(void) {
ios::sync_with_stdio(false); cin.tie(nullptr);
int t=1;// cin>>t;
while (t--) solve();
return 0;
}
E:零域(思维+二分)
题意:
给定一个 n 和 m,求[1 - n]中 有多少个数的阶乘末尾有m个0。
思路:
注意 n 和 m 很大,思维题 or 数位dp or 矩阵,很显然出题组不会考后面两个( )
直接开玩,枚举发现,每当遇到一个5的倍数,就会多一个0。
ok写完了直接交题。
ok喜提 wa
哦,不小心给0也算进去了,给0删了再交一发。
ok喜提 wa
肯定是我枚举的方法不对,重构一遍代码再交一发。
ok喜提 wa
如果你也红温了评论区请扣111
后来我们发现,25的时候会多出两个0,很简单的道理,两个5,在前面激情wa的可能是小丑吧()
回归正题。
所以我们需要预处理5的所有幂数,然后使用二分去查找第一次出现m个0的数,锁定之后,向后查找5个数(为什么是5个数,因为更多的数一定会遇到新的5)。
把这5个数都check一遍,合法的都加进答案里面。
还有一个坑点,奶奶的这题卡常,给我十连评测都卡出来了,ok喜提tle,在我细节优化之后,也是成功通过了。
时间复杂度O(log5(1e18) * log2n * T)
AC代码:
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
constexpr i64 N = 1e18;
int cnt;
i64 p5[1001];
void solve() {
i64 n, m; cin>>n>>m;
auto check = [&](i64 x) {
i64 ans = 0;
for(int i=1; i<cnt; ++i) {
i64 y = x/p5[i];
if(y == 0) break;
ans += y;
}
return ans;
};
i64 l = 0, r = n;
while(r-l > 1) {
i64 mid = l+r >> 1;
if(check(mid) >= m) r = mid;
else l = mid;
}
vector<i64> v;
for(i64 i=r; i<=min(n, r+5); ++i) {
if(check(i) == m) {
v.push_back(i);
}else {
break;
}
}
if(v.size() == 0) {
cout<<"-1\n";
}else {
cout<<v.size()<<"\n";
for(auto x: v) {
cout<<x<<" ";
} cout<<"\n";
}
}
signed main(void) {
ios::sync_with_stdio(false); cin.tie(nullptr);
for(i64 i=1; N/i>=5; i*=5) {
p5[cnt++] = i;
}
int t=1; cin>>t;
while (t--) solve();
return 0;
}
F:神秘三人组(枚举+前缀和)
题意:
给定一个长度为 n 的数组,和一个 p。
找长度为3。公比为p的子序列的个数。
思路:
直接枚举肯定超时。
枚举第一个数或最后一个数不能保证另外两个数的顺序,因此枚举中间的数,保证统计的子序列不重复,并且有序。
所以我们需要时刻保存前缀和后缀。
枚举时,在后缀中查找a[i] * p的数量,在前缀中查找 a[i]/p的数量,如果a[i]%p != 0 直接跳过。
合法时直接 ans += 前缀合法数量 * 1 * 后缀合法数量。
时间复杂度O(nlog2n)
AC代码:
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
int n, p; cin>>n>>p;
map<i64, i64> mp1, mp2;//mp1为前缀,mp2为后缀
vector<int> a(n+1);
for(int i=1; i<=n; ++i) {
cin>>a[i];
mp2[a[i]]++;
}
i64 ans = 0;
for(int i=1; i<=n; ++i) {//枚举中间的数
mp2[a[i]]--;
if(a[i]%p == 0) {
i64 pre = 1ll * a[i] / p;
i64 suf = 1ll * a[i] * p;
if(mp1.count(pre) and mp2.count(suf)) {
ans += mp1[pre] * mp2[suf];
}
}
mp1[a[i]]++;
}
cout<<ans<<"\n";
}
signed main(void) {
ios::sync_with_stdio(false); cin.tie(nullptr);
int t=1; //cin>>t;
while (t--) solve();
return 0;
}
G:ICPC上海悲剧时刻(贪心)
题意:
给定一个长度为 n 的数组。
你可以选择任意 k 个数(每个数只能用一次)相乘后放回原数组。
求原数组最大值。
思路:
排序后。
如果最大的 k 个数中有 0 直接数组目前数组最大的数。
否则,输出最大的 k 个数之积。
时间复杂度O(nlog2n)
AC代码:
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
constexpr int M=998244353;
void solve() {
int n, k; cin>>n>>k;
vector<i64> a(n+1);
for(int i=1; i<=n; ++i){
cin>>a[i];
}
sort(a.begin()+1, a.end());
i64 ans = 1;
for(int i=n; i>n-k; --i) {
if(a[i] == 0) {
cout<<a[n]%M<<"\n";
return ;
}
ans = ans * a[i]%M;
}
cout<<ans<<"\n";
}
signed main(void) {
ios::sync_with_stdio(false); cin.tie(nullptr);
int t=1; //cin>>t;
while (t--) solve();
return 0;
}
===
H:等等...这是dp对吧(贪心+枚举)
题意:
给定一个长度为 n 的仅包含 'd' 和 'p' 字符串 s。
对于这个字符串的任意子串s[l, r],你可以180°翻转它(相当于先reverse 然后 'p'会变成‘d’, 'd'会变成'p'),得到F(s[l, r])
然后你可以选择任意一个子串,用他的F(s[l, r]) 替换 s[l, r],求最小字典序的串。
当然也可以不操作。
思路:
看见最小字典序直接贪心。
从前到后找第一个不是 d 的位置,然后将该位置固定为左端点 l ,枚举右端点 r ,
依次尝试把F(s[l,r])替换进来,对答案取min。
时间复杂度O(n2)
AC代码:
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
int n; cin>>n;
string s; cin>>s;
string ans = s;
string a1 = s;
for(int i=0; i<n; ++i) {
if(a1[i] == 'p') {
for(int j=i; j<n; ++j) {
for(int k=i; k<=j; ++k) {
a1[k] = (s[j+i-k]=='p'?'d':'p');
}
ans = min(ans, a1);
}
break;
}
}
cout<<ans<<"\n";
}
signed main(void) {
ios::sync_with_stdio(false); cin.tie(nullptr);
int t=1; //cin>>t;
while (t--) solve();
return 0;
}
I:坐得下吗(思维)
题意:
给定 n 组人,要么是 1个, 要么是两个。
给定一排长度为 L 的座位 (所有人数之和必然等于L)
n组人依次来占座位,在最坏情况下是否能保证每一组人都能连坐在一起。
思路:
思考发现,只有当后面的人是2个的时候可能会做不到一起。
直接把前面的每组都想象的比较邪恶,每组都隔一个位置再坐,这样可以让后面2个人一起的队伍没法插空。
相当于把前面每一组占用的座位都+1
当最后没法隔着坐,并且挤不下时,判断还有没有2人一组的队伍还没坐,如果有就是“No”
否则是"Yes"
需要注意一下,如果最后剩了两个座位,并且刚好是两人一组过来,他们是可以做得下的,只不过他们占用的座位就没办法+1了。
时间复杂度O(n)
AC代码:
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
int n, L; cin>>n>>L;
for(int i=1; i<=n; ++i) {
int x; cin>>x;
if(L >= x+1) L -= x+1;//隔着坐能否坐下
else if(L >= x) L -= x;//最后挤一挤能否坐下
else {//只能挤着坐了,且空隙全为1,出现2人一起的就寄
if(x == 2) {
cout<<"No\n";
return ;
}
}
}
cout<<"Yes\n";
}
signed main(void) {
ios::sync_with_stdio(false); cin.tie(nullptr);
int t=1; //cin>>t;
while (t--) solve();
return 0;
}
J:好长的序列(数学+枚举)
题意:
给定一个长度为 n 的数组和一个 x。
数组会循环延长,数组前缀的第几位能超越x
思路:
开一个ans直接循环加数组会tle。
优化一下枚举方法:
求出一个循环数组的总和 sum,x/sum 得到会完整经过 cnt 次循环数组 。
now = cnt * sum,t = n * cnt
用处理好的次数再去再枚举加上一遍数组的每一位,直到大于x时结束。
时间复杂度O(n)
AC代码:
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
int n; cin>>n;
i64 sum = 0;
vector<int> a(n+1);
for(int i=1; i<=n; ++i) {
cin>>a[i];
sum += a[i];
}
i64 x; cin>>x;
i64 cnt = x / sum;
i64 t = cnt * n;
i64 now = cnt * sum;
while(now <= x) {
now += a[t%n+1];
t++;
}
cout<<t<<"\n";
}
signed main(void) {
ios::sync_with_stdio(false); cin.tie(nullptr);
int t=1;// cin>>t;
while (t--) solve();
return 0;
}
K:城市链接(并查集)
题意:
给 n 个点, m个操作,
操作一 联通 a b 点(联通是会传递的)
操作二 询问 a b 是否联通
思路:
并查集板子
时间复杂度O(n+m)
AC代码:
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
int f[10001];
int Find(int x) {
if(x == f[x]) return x;
return f[x] = Find(f[x]);
}
void Union(int x, int y) {
int fx = Find(x), fy = Find(y);
if(fx == fy) return ;
f[fx] = fy;
}
void solve() {
int n, m; cin>>n>>m;
for(int i=1; i<=n; ++i) f[i] = i;
for(int i=1; i<=m; ++i) {
int op, a, b; cin>>op>>a>>b;
if(op == 1) {
T.Union(a, b);
}else {
if(T.Find(a) == T.Find(b)) {
cout<<"Y\n";
}else {
cout<<"N\n";
}
}
}
}
signed main(void) {
ios::sync_with_stdio(false); cin.tie(nullptr);
int t=1; //cin>>t;
while (t--) solve();
return 0;
}
L题黑题(tag3000),不可做题。