补题入口直接点题目即可
这场的题目更偏向思维,很多题目需要转变一下大脑子才可以想出来
J-魔法数字
题目:
一个数字,当这个数的位数不少于2时就将它变为各个位数上的数字相加,问会进行多少次操作,并且这个数字最终会变成多少
思路:
直接用循环去模拟这个过程就好了,输入的数字大小1e64所以用字符串去存,后续64位数加起来数也不会很大直接用变量存起来,然后把它的每一位存到字符串,然后再用这个字符串去判断长度和计算每一位的求和,最后输出即可
代码:
cpp
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr)
#define endl '\n'
void slove(){
string s; cin>>s;
int m=0;
int i=0;
if(s.size()==1) m=s[0]-'0';
while(s.size()>=2){
m=0;
for(char x:s){
m+=x-'0';
}
string ss="";
int p=m;
while(p>0){
int n=p%10;
ss+='0'+n;
p/=10;
}
s=ss;
i++;
}
cout<<i<<" "<<m<<endl;
}
signed main(){
IOS;
int t=1;
cin>>t;
while(t--) slove();
}
H-wnd的魔法背包
这题也很简单,就是给n个数其中有正有负,你最多能进行n/2从操作把一个数从负变为正,求能得到的最大总和
只要把负数都存起来,排序后尽量把小的负数变为正数就可以使总和最大,可是赛时wa了两发,因为忘记判断没有负数全是正数的情况导致数组越界了
代码:
cpp
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr)
#define endl '\n'
void slove(){
int n; cin>>n;
int m=n/2;
vector<int> a;
int num=0;
for(int i=0;i<n;i++){
int x; cin>>x;
if(x<0) a.push_back(x);
else num+=x;
}
sort(a.begin(),a.end());
if(a.size()!=0){
for(int i=0;i<m;i++){
num+=-a[i];
if(i==a.size()-1) break;
}
}
cout<<num;
}
signed main(){
IOS;
int t=1;
//cin>>t;
while(t--) slove();
}
I-遗迹核心的临界容差
题目:
有N座储能核心,每个核心对应一个强度值Ai,每两座核心之间相连,如果两座核心之间差值的绝对值大于阈值X,那能量线路就会不稳定,所以要在这两个之间插入一个隔离锚,这样就把线路分成了K段,求最小的阈值X是多少可以满足只需要插入不超过K-1个隔离锚(也就是把序列分成不超过 K 段)
输入:第一行N和K表示核心总数和最大分段数,第二行N个整数表示没座核心的强度值
输出:最小阈值X
思路:
典型的二分答案,结合题目可以得知X具有单调性,X越大需要放的锚就越少,题目又要最小可行的X,二分答案1到1e9每次去拿算出的阈值去检查能不能满足题目
代码:
cpp
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
#define endl '\n'
#define pii pair<int,int>
#define fi first
#define se second
int n,k;
vector<int> c;
bool check(int X){//检查是否符合题目
int num=0;
for(int i=0;i<c.size();i++){
if(X<c[i]) num++;
}
if(num>k){
return false;
}else{
return true;
}
}
void solve(){
cin>>n>>k;
k--;//k-1个锚分成k段
vector<int> a(n);
for(int i=0;i<n;i++) cin>>a[i];
if(n==1){
cout<<0;
return ;
}
for(int i=1;i<n;i++) c.push_back(abs(a[i]-a[i-1]));//直接存差值
int l=0,r=1e9;
while(l<r-1){
int mid=(l+r)>>1;
bool zz=check(mid);
if(zz) r=mid;//成立就缩小
else l=mid+1;//不成立就放大
}
if(check(l)){//退出后两个相邻做判断
cout<<l;
}else{
cout<<r;
}
}
signed main()
{
IOS;
int T = 1;
// cin >> T;
while(T --) solve();
return 0;
}
G-wnd的魔法排序
题目:
一个从1到n的排列,其中任意两个相邻的数之和都是质数,找出所有满足的排列,按字典序从小到大输出,2 <= n <= 11
思路:
从1开始枚举所有排列,如果下一位之和是质数就可以放(标记这个数)然后接着往后dfs下一位,如果不可以就返回到上一层将这个标记撤销从下一位开始,接着去dfs,如果够了n个数就终止,输出,之后返回到上一层(撤销对当前数的标记)再从下一个数开始遍历,时刻保持字典序最小,结合代码理解一下应该还可以
cpp
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
#define endl '\n'
#define pii pair<int,int>
#define fi first
#define se second
int n;//排列的长度
int ans[20];//用来存答案的数组
bool b[20];//标记当前数是否用过
bool check(int x){//判断素数
if(x==2) return true;
for(int i=2;i<=sqrt(x);i++){
if(x%i==0) return false;
}
return true;
}
void dfs(int step){
if(step==n+1){//如果答案数组长度等于n了那就全放进去了输出
for(int i=1;i<=n;i++){
cout<<ans[i]<<" ";
}
cout<<endl;
return ;
}
for(int i=1;i<=n;i++){
if(b[i]) continue;
if(step==1||check(ans[step-1]+i)) ans[step]=i;
else continue;
b[i]=1;//这个数用过就标记上
dfs(step+1);//除了用过的数再接着递归其他的数
b[i]=0;
//最终返回到这撤销上一个使用的数的标记,然后循环从下一个数在开始这个数后续就可以再使用
}
}
void solve(){
cin>>n;
dfs(1);//从1开始调用dfs
}
signed main()
{
IOS;
int T = 1;
// cin >> T;
while(T --) solve();
return 0;
}
比赛时还是不会用dfs,看到这题只想着全排列算出来可是最后会超时,还是要掌握递归的技巧和代码的书写
A-能量任务
题目:
有 n 个任务,执行第 i 个任务前,你的当前能量必须不少于 hi ,任务完成后,能量变化 di ,即变为当前能量 + di ,每个任务必须且只能执行一次,你可以自由决定执行顺序,所有时刻能量都不能为负,已知 hi+di≥0,因此只要执行前满足门槛,执行后就不会立刻变成负数,求完成所有任务所需的最小初始能量
思路:
主要还是因为题目中hi+di≥0 这个条件,既然满足门槛执行后就不会变成负数,所以**hi一定为正并且加上负的 di**也不会小于0,所以负能量时就有以下规则
d < 0(负能量任务) :执行后能量减少,这类任务应该先做"门槛减去消耗"较大的 ,也就是h + d大的先做。因为h + d表示执行完这个任务后剩余的能量,如果这个值大,说明这个任务虽然消耗能量,但执行后剩余能量依然较多,可以支撑后续更严格的任务d >= 0(正能量任务) :执行后能量增加或不变,这类任务应该先做门槛低的,因为能量会越做越多,先做低门槛的可以安全积累能量,再去挑战高门槛的
正能量 :按h最小到大升序,优先处理门槛小的
负能量 :按h+d降序,h+d越大执行后剩余能量越多,后面也可以用上
代码:
cpp
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define pii pair<int, int>
#define fi first
#define se second
#define IOS ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr)
#define endl '\n'
void solve(){
int n;
cin >> n;
vector<pii> add, sub;
for(int i = 0; i < n; i++){
int h, d;
cin >> h >> d;
//按照能量变化分组
if(d >= 0)
add.emplace_back(h, d);
else
sub.emplace_back(h, d);
}
sort(add.begin(), add.end());//排序
sort(sub.begin(), sub.end(), [](pii a, pii b){
return a.fi + a.se > b.fi + b.se;
});
int cur = 0;
int ans = 0;
for(auto &p : add){//正能量从门槛最小的开始
int h = p.fi, d = p.se;
if(cur < h){
ans += h - cur;
cur = h;
}
cur += d;
}
for(auto &p : sub){//先执行大的后面剩余的能量可以作用到后面
int h = p.fi, d = p.se;
if(cur < h){
ans += h - cur;
cur = h;
}
cur += d;
}
cout << ans << endl;
}
signed main(){
IOS;
int t = 1;
while(t--) solve();
return 0;
}
赛时想出了这个贪心可是排序的细节写错了,要增进对stl容器的熟练运用
B-三色平衡
题目:
给定一个只包含字符 0、1、2 的字符串 s
一个子串是 "平衡的" ,当且仅当字符 0、1、2 在该子串中出现次数相同
求最长平衡子串的长度,空串也视为平衡,因此无解时答案为 0
思路:
对字符串进行前缀统计, a、b、c 分别表示从开头到当前位置为止,字符 0、1、2 各自出现的次数,现在定义一个状态
(a - b, b - c)- 如果两个位置
i和j(i < j)的状态相同,那么说明从i+1到j这段子串中,0、1、2数量的增量相等 ,即:- aj - ai = bj - bi
- bj - bi = cj - ci
由此就可以推出三个字符数量相等,对于每个状态,记录它第一次出现的位置。当后面再次遇到相同状态时,当前位置与第一次出现位置之间的长度就是一个候选答案,更新最大值
代码:
cpp
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr)
#define endl '\n'
#define pii pair<int,int>
#define fi first
#define se second
void slove(){
int n;
cin>>n;
string s; cin>>s;
int a=0,b=0,c=0;//储存出现数量
map<pii,int> mp;
mp[{0,0}]=-1;
int ans=0;
for(int i=0;i<n;i++){
if(s[i]=='0') a++;//统计
else if(s[i]=='1') b++;
else c++;
pii t={a-b,b-c};//储存状态
if(mp.find(t)!=mp.end()) ans=max(ans,i-mp[t]);
//找到状态一样的说明这个区间中符合abc都相等,更新最大值
else mp[t]=i;//没找到就存进去是新的
}
cout<<ans<<endl;
}
signed main(){
IOS;
int t=1;
//cin>>t;
while(t--) slove();
}
这题就是利用线性遍历的特征,前缀状态是跟随从左往右扫描变化的
感觉这种题很经典:前缀状态 + 哈希表 + 一次扫描
这个模式的关键是:将区间条件转化为前缀状态的等价关系,然后用线性扫描配合哈希表记录状态首次出现的位置,就能快速得到答案
E-传话
题目:
n个人编号从1到n,对于每个人i都有一个固定传话人pi,pi知道了消息会传给i,单向边,pi可以重复也允许pi=i,因此每个人只有一个消息来源,而一个人可以把消息传给多人,一个人可以重复收到消息,需要选一个人,最后让知道消息的人最多
思路:
理解一下从第一个人开始一直往后找的话,找到最后一个人他再找不管哪一个人一定会形成一个环,题目说了只能有一个消息来源所以就不会出现多个环挨在一起共用边的情况(如果共用就一定会有两个点汇集到一个点)所以选择环上任意一人都可以传遍整个环,所以只需要找到长度最大的环就可以让知道消息的人最多,这样就可以忽略方向用并查集去求弱连通环的大小
代码:
cpp
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int fa[N];// fa[i] 存 i 的"上级"(父节点)
int sz[N];// sz[i] 存以 i 为根的集合大小(只有根节点有效)
// 查找 x 所在集合的根,并做路径压缩
int find(int x) {
if (fa[x] == x) return x;// 自己就是根
return fa[x] = find(fa[x]);// 递归找根,并直接指向根
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
int n;
cin >> n;
// 第一步:初始化,每个人自成一个集合
for (int i = 1; i <= n; i++) {
fa[i] = i;// 自己的上级是自己(根)
sz[i] = 1;// 自己一个元素
}
// 第二步:读入每个人的来源,并合并
for (int i = 1; i <= n; i++) {
int p;
cin >> p;// p 是 i 的消息来源
int a = find(i);// 找到 i 所在集合的根
int b = find(p);// 找到 p 所在集合的根
if (a == b) continue;// 已经在同一集合(说明这条边形成了环),跳过
// 按大小合并:把小集合挂到大集合上
if (sz[a] < sz[b]) swap(a, b);// 确保 a 是大集合
fa[b] = a;// 让 b 的根指向 a(即 b 集合并入 a)
sz[a] += sz[b];// 更新 a 集合的大小
}
// 第三步:找出最大的集合
int ans = 0;
for (int i = 1; i <= n; i++) {
if (fa[i] == i) {// 只有根节点才代表一个完整的集合
ans = max(ans, sz[i]);
}
}
cout << ans << '\n';// 输出答案
}
return 0;
}
C-余数清理
题目:
给定一个长度为 n 的非负整数序列和一个正整数 m,你必须删除一个非空连续区间,并且至少保留一个元素。要求删除后剩余所有元素之和能够被 m 整除。请最大化剩余元素的数量。如果不存在合法方案,输出 −1
注意:删除区间后,区间左侧和右侧的元素都会保留,但不要求它们在原序列中相邻
思路:
设所有元素和为S,删除区间和为x
则合法状态为**(S-x)mod m=0**等价于 x mod m = S mod m
最大化剩余的元素数量,就是找满足以上条件的最短子数组
这里就会发现和B题非常类似,我认为这两道题可以归为一种题型,我们定义一种前缀去表示区间的状态
令前缀和模 m 为 pi,其中 p0=0,子数组 [l,r] 的和模 m 为:
(pr−pl−1) mod m
记目标余数为 target=S mod m,所以有:
- targe=(pr−pl-1) mod m
- pr−pl-1=targe mod m
- −pl-1=(targe - pr)mod m
最终-1挪过去得到
pl-1 = ( pr−targe +m ) mod m(加m避免负数取模的情况)
这个公式是为了方便找区间是否合法,如果你用 targe=(pr−pl-1) mod m去算,那每次都要遍历l到r算一遍就会超时,我们公式变形为对于每个右端点合法前缀余数前面有没有出现过就可以了,这样就可以用哈希表去查找,将每一个区间状态都记录在哈希表里,然后利用公式去找符合条件的区间和区间长度
代码:
cpp
#include<bits/stdc++.h>
#define int long long
using namespace std;
void solve(){
int n,m;cin>>n>>m;
vector<int>a(n+1),p(n+1);
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)p[i]=(p[i-1]+a[i])%m;//算前缀和
int tar=p[n];//所有元素总和
int len=n+1;
vector<int>lst(m,-1);//数组表示哈希表,只需要证明是否存在
lst[0]=0;
for(int i=1;i<=n;i++){
int need=(p[i]-tar+m)%m;//计算当前右端点合法的左端点
if(lst[need]!=-1&&i-lst[need]<n)len=min(len,i-lst[need]);
//判断左端点是否存在并且更新最小长度
lst[p[i]]=i;//将当前右端点在哈希数组中更新为存在
}
if(len>n)cout<<"-1\n";//一个都删不了输出-1
else cout<<n-len<<"\n";//剩余元素
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;cin>>T;
while(T--)solve();
return 0;
}
通过B和C题,有些感受到这类题的特点,如果遍历每一个双重循环就会很慢,所以前缀和哈希表单配使用就可以优化到O(n),线性扫描一次
- 运用前缀和,把区间条件转变为两个前缀状态之间的关系
- 哈希表直接精准定位
键:前缀状态的值
值:该状态出现的位置- 线性扫描一次
利用公式变形计算出期望的前缀状态
在哈希表中查找,并更新答案
将新的前缀状态更新到哈希表中
总结
温故而知新(温习旧的知识 发现都是新的)