4 2025 04 26 模拟赛总结
成绩表
![[12 题解/photo/{C872C414-70A3-427C-8A97-864E7898AC31}.png]]
做题情况
- T1:水题一道,100pts
- T2:细节很多的水题(花了很长时间才写完,感觉细节很多很难写),100pts
- T3:赛时感觉像是字符串kmp一类的东西,赛后看题解发现是一个用各种stl写的贪心,赛时打的暴力也没拿上分,0pts
- T4:场上想到了大概的解法,但是没有时间了,所以只打了一个暴力,10pts
赛时心态
这场模拟赛的瓶颈大概是T2,赛场上做这题的时候感觉很痛苦,想到要处理一种情况,然后突然又想到其他很多情况,感觉很难处理,赛后看赛时代码,感觉还可以,不算太麻烦,就是要考虑清楚每种情况该如何处理。
T3这道题再给时间恐怕也做不出来,因为题解中所使用的string,set,我都不太熟悉。
T4这道题再给点时间我应该能再打点部分分,赛时的大致思路与题解一致,但实现在赛时大概是写不出来的
题面及题解
T1
题面
给定n个数,任意选择两个不同的数进行一次(不能不用)\(a\bmod b=c\)的操作,将\(a\)替换为\(c\)
求新的\(n\)个数字的和最大为多少
题解
这里注意,如果\(n\)个数的权值相同,那么减掉一个权值,否则我们一定能找到\(a<b\)使得\(a\bmod b=a\),求和不变
code
typedef long long ll;
const int N = 2e5 + 10;
int n;
int a[N];
ll sum;
int main() {
scanf("%d", &n);
for(int i = 1; i <= n; i++)
scanf("%d", &a[i]), sum += a[i];
int cnt = unique(a + 1, a + 1 + n) - a - 1;
if(cnt == 1) {
sum -= a[1];
}
printf("%lld\n", sum);
return 0;
}
T2
题面
\(n\)个数字,我们要从中选出一些数来使得选出数的乘积最大(至少选一个),求选了哪些数。
题解
这题最大的特点是细节很多,我们必须考虑到每个可能会导致答案出错的小细节。
我的做法是先记录下来有多少个负数以及多少个正数,然后将原数组按照绝对值从小到大排序,然后根据负数个数的奇偶分别处理。
需要注意的点
- 必须至少选择一个数,即使这个数可能是\(0或1或一个负数\)
- 多出来的\(1和-1\)不选
最难处理的是如何处理多出来的\(-1和1\)
- 先看1
我们要考虑当前已经选择的数,如果当前遍历到1时还没有选择任何一个数,那么我们这个1应该放入答案数组,因为当前1是最大的 - 再看-1
我们要考虑当前已经选择的负数的数量,如果当前选择的负数数量为偶数,那么说明我们已经选择的负数的符号已经能够相互抵消,那么当前-1对于答案来说没有贡献。同样这里的-1也要考虑数组中没有元素的情况
时间复杂度\(O(n\log n)\)
具体实现看代码
code
const int N = 1e5 + 10;
int n;
int a[N], b[N];
vector<int> ans;
bool cmp(int a, int b) {
if(abs(a) == abs(b)) return a < b;
return abs(a) < abs(b);
}
int main() {
scanf("%d", &n);
int cnt = 0, cntt = 0;
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
b[i] = a[i];
if(a[i] < 0) cnt++;//记录负数个数
if(a[i] > 0) cntt++;//记录正数个数
}
sort(a + 1, a + 1 + n, cmp);
if(cnt & 1) {
//如果只有一个负数,并且没有正数,我们要将0以及负数放入考虑范围内
if(cnt == 1 && cntt == 0) {
int mx = -1e9 - 10;
for(int i = 1; i <= n; i++) {
mx = max(mx, a[i]);
}
ans.push_back(mx);
} else {
for(int i = n; i >= 1; i--) {
//当前的0没有利用价值,因为按照绝对值排序,到0说明已经到数组末尾
if(a[i] == 0) break;
if(a[i] == 1) {
if((int)ans.size() > 0) continue;
else {
//如果答案数组中没有元素,那么1有价值
ans.push_back(a[i]);
break;
}
}
if(a[i] == -1) {
//数组中有元素,并且现在用的负数个数为偶数
if((cnt & 1) && (int)ans.size() > 0) {
continue;
} else {
ans.push_back(a[i]);
cnt--;
}
}
if(a[i] > 1) ans.push_back(a[i]);
if(a[i] < -1) {
//如果不是最后一个负数,则可以放
if(cnt != 1) {
ans.push_back(a[i]);
cnt--;
} else {
continue;
}
}
}
}
} else {
//0有价值
if(cnt == 0 && cntt == 0) {
int mx = -1e9 - 10;
for(int i = 1; i <= n; i++) {
mx = max(mx, a[i]);
}
ans.push_back(mx);
}
for(int i = n; i >= 1; i--) {
if(a[i] == 0) break;
if(a[i] == 1) {
if((int)ans.size() > 0) continue;
else {
ans.push_back(a[i]);
break;
}
}
if(a[i] == -1) {
if((cnt % 2 == 0) && (int)ans.size() > 0) {
continue;
} else {
ans.push_back(a[i]);
cnt--;
}
}
if(a[i] > 1) ans.push_back(a[i]);
if(a[i] < -1) {
ans.push_back(a[i]);
cnt--;
}
}
}
sort(ans.begin(), ans.end());
for(int i = 0; i < (int)ans.size(); i++) {
printf("%d ", ans[i]);
}
printf("\n");
return 0;
}
T3
题面
给定一个只由小写字母构成的字符串\(t\),初始时\(t\)为白色
给出\(n\)个字符串\(s\)
我们可以进行无限次操作,每次选定\(s_{i}\),将\(t\)中与\(s_{i}\)完全相同的字串染成黑色
求最多能把\(t\)中多少字符染成黑色
\(t_{len}\leq 2\times 10^{5},\,s_{len} \leq 10\)
\(1\leq n\leq 10^{5}\)
题解
这题不会做,后来是看着题解改对的,那我就来说说题解的思路吧
首先,有一种很显然的暴力算法,\(dp[i]\)表示以\(i\)结尾的字符串最多能染色多少个字符,遍历\(s_{i}\),遍历\(t\),取出以\(s_{i}\)的最后一个字符结尾的与\(s_{i}\)长度相同的子串,然后判断两个串是否相等,时间复杂度\(O(|s|\times n\times 10)\)
可以进行优化,因为\(s_{i}\)的长度只有10种,因此我们可以每次去除一个长度为\(x(1\leq x\leq 10)\)的子串,然后判断是否存在\(s_{i}\)与这个子串相等。判断可以用set暴力判断,时间复杂度为\(O(|s|\times 10\times \log n \times 10)\)。
code
int n;
string t;
set<string> st;
int main() {
cin >> t >> n;
for(int i = 1; i <= n; i++) {
string s;
cin >> s;
st.insert(s);
}
t = " " + t;
int m = t.size();
vector<int> dp(m + 5);
for(int i = 1; i <= m; i++) {
dp[i] = dp[i - 1];
string s;
for(int j = i; j >= max(0, i - 10); j--) {
s = t[j] + s;
if(st.count(s)) {
dp[i] = max(dp[i], dp[j - 1] + (int)s.size());
}
}
}
printf("%d\n", dp[m]);
return 0;
}
T4
题面
给定一棵以1为根的树,n个节点,定义一棵子树为好子树,当且仅当该子树的所有节点权值乘积的因子数量\(\geq k\),求这棵树上有多少棵子树是好子树
\(1\leq n\leq 2 \times 10^{5},\,1\leq k\leq 10^{18}\)
\(1\leq a_{i}\leq 10^{6},\,1\leq u,\,v\leq n\)
题解
这题赛时想到了用质因数来求每个子树的因数个数,但是没时间写了,而且也没有那么熟悉STL,所以最后只拿了\(a_{i}\leq 3\)的10pts
下面说下思路,题解写的太恶心,所以我结合自己的思路,再捋一遍
首先我们要预处理出每个数分解质因数以后的结果,也就是每个数分别有哪些质因数、以及每个质因数的指数是多少
预处理的时间复杂度为\(O(M\log \log M)\),这是筛法的典型复杂度,由调和级数求和可得
void init() {
for(int i = 2; i <= 1e6; i++) {
if(p[i].size())
continue;
for(int j = i; j <= 1e6; j += i) {
int x = j;
int t = 0;
while(x % i == 0) {
x /= i;
t++;
}
p[j].push_back({i, t});
}
}
}
然后对这棵树进行dfs,因为是乘积,所以可以转化为质因子相加
启发式合并(把小的加到大的上面)当前节点与其子节点,而后算一下每个质因子对答案的贡献,看是否\(\geq k\)即可
合并这里是时间复杂度瓶颈,每个元素至多被合并\(O(\log n)\)次,每次合并的时间复杂度为\(O(\log s)\),\(s\)为当前节点的\(mp.size()\),那么总的时间复杂度为\(O(n\log^{2}n)\)。
设\(d[x]\)为\(x\)节点的因子个数,\(p_{i}\)表示\(i\)的指数
code
typedef long long ll;
const int N = 2e5 + 10, M = 1e6 + 10;
int n;
ll k;
int h[N], ver[N << 1], ne[N << 1], tot;
vector <pair<int, int> > p[M];
map <int, int> mp[N];
int a[N];
ll mul[N], ans;
void add(int x, int y) {
ver[++tot] = y;
ne[tot] = h[x];
h[x] = tot;
}
void init() {
for(int i = 2; i <= 1e6; i++) {
if(p[i].size())
continue;
for(int j = i; j <= 1e6; j += i) {
int x = j;
int t = 0;
while(x % i == 0) {
x /= i;
t++;
}
p[j].push_back({i, t});
}
}
}
void dfs(int x, int fa) {
//将x节点自己的权值的贡献加入mp
for(auto o : p[a[x]]) {
mp[x][o.first] += o.second;
}
//分别加儿子的贡献
bool fn = 0;
for(int i = h[x]; i; i = ne[i]) {
int y = ver[i];
if (y == fa) continue;
dfs(y, x);
if(mul[y] >= k) {
fn = 1;
}
if(mp[x].size() < mp[y].size())
swap(mp[x], mp[y]);
for (auto o : mp[y]) {
mp[x][o.first] += o.second;
}
}
//统计因数个数
if(fn) {
ans++;
return;
}
mul[x] = 1;
for(auto o : mp[x]) {
mul[x] *= o.second + 1;
if(mul[x] >= k)
break;
}
ans += mul[x] >= k ? 1 : 0;
}
int main () {
init();
scanf("%d%lld", &n, &k);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
for(int i = 1; i < n; i++) {
int u, v;
scanf("%d%d", &u, &v);
add(u, v);
add(v, u);
}
dfs(1, 0);
printf("%lld\n", ans);
return 0;
}