几天前的这组题,算是我为数不多的oi centests经历中比较水的一次比赛了(毕竟所有题的代码都奇短无比)。如果不是因为D题被卡得恶心许久不然全解还是……没可能的。
个人觉得这场比赛不浪费生命的是后面三道题。
A. Night At the Museum
题目大意:
给出一个转盘,上面从 ′a′ 到 ′z′ 依次顺时针排布。一开始指针指向 ′a′ ,之后对于给定的一段字符串,要求得到正确输出该字符串的最小转动距离总和(即每次转到某个字母要尽量地快)。
个人感觉Codeforces的前半部分题(只是指Div.2)是用来练手速的。如何快速的抽离出题目状态并最快地进行模拟?这或许也是刷这类水题的意义所在。
Code :
#include <bits/stdc++.h>
using namespace std;
string str;
int main(){
cin>>str;
int pre=0,step=0;
for(int i=0;i<str.size();i++){
int a=str[i]-'a';
step+=min((a-pre+26)%26,(pre-a+26)%26);
pre=a;
}
cout<<step;
}
B. Coupons and Discounts
题目大意:
有两种优惠方案:一天买两个披萨或者连续两天都买一个披萨。现给出每天要买的披萨数量(可能为0),要求全部用上述两个优惠买,且不能买多。求是否可以通过合理安排顺序,使得上述要求能够被满足。
关键在于一个 有点玄学的 贪心:
- 当遇到购买披萨数量为0或者最后一天的时候,前面的披萨个数必须要是偶数。
显然的是对于一段连续的日子,只要总和是偶数,那么一定可以用上述优惠方式“覆盖”。那么遇到一个不购买披萨的日子就判断一下好惹。
Code :
#include <bits/stdc++.h>
using namespace std;
int main(){
int n,pre=0;
cin>>n;
bool win=true;
for(int i=1,x;i<=n+1;i++){
if(i<=n)cin>>x;
if(!x&&pre){win=false;break;}
pre=(pre+x)%2;
}
puts(win?"YES":"NO");
return 0;
}
C. Socks
题目大意:
对于接下来的n天,每天都从原来m大小的袜子堆中取出一对袜子穿。由于特殊原因取出哪些袜子的编号是不能改变的,只能改变袜子的颜色。求最少改变多少只袜子的颜色,使得接下来的n天每天穿的袜子颜色都相同。
构图。每一组的袜子颜色都要相同,而如果有两组之间出现重复袜子的,说明这两组内的袜子颜色也都要相同。于是我们对每一组都建边,此时在同一个连通块内的袜子颜色都要相同。根据贪心,我们只需要染当前连通块的最多出现颜色即可。
Code :
#include <bits/stdc++.h>
#define M 200005
using namespace std;
int col[M];
vector<int>G[M];
void add_edge(int u,int v){
G[u].push_back(v);
G[v].push_back(u);
}
bool used[M];
map<int,int>Mp;
void dfs(int c){
used[c]=true;Mp[col[c]]++;
for(int j=0;j<G[c].size();j++){
int v=G[c][j];
if(!used[v])dfs(v);
}
}
int main(){
int n,m,k;
scanf("%d %d %d",&n,&m,&k);
for(int i=1;i<=n;i++)scanf("%d",&col[i]);
for(int i=1,u,v;i<=m;i++){
scanf("%d %d",&u,&v);
add_edge(u,v);
}
int ans=0;
for(int i=1;i<=n;i++)
if(!used[i]){
Mp.clear();
dfs(i);
map<int,int>::iterator it=Mp.begin();
int cnt=0,sum=0;
while(it!=Mp.end()){
sum+=it->second;
if(cnt<it->second)cnt=it->second;
++it;
}
ans+=sum-cnt;
}
cout<<ans;
}
D. 80-th Level Archeology
题目大意:
给定由多个序列组成的密码串。将密码串旋转c位的意思就是对于每个数都有操作 ai=(ai+c−1)%C+1(0≤c<C) 。开启密藏的方法是使得上述密码串中的多个序列按照顺序字典序是从大到小的。求出c的一个可能解。
整个序列按照字典序排序 → 相邻两个序列按照字典序有序即可。
对于相邻两个序列,随着c的不断改变,满足前面的序列比后面序列字典序小的情况是一段区间,即只有在某段区间内才能更新c,而其他区间不能更新c。所以我们只需要计算出相邻序列之间的可行解区间,再依次合并过来。
然后这里就卡的有点久,因为发现比较难直接合并可行区间。类似的有POI2015_Kurs_szybkiego_czytania,所以给出此处 非常水 的处理方法(按照Claris大神的说法这就算扫描线?):
- 合并不可行区间,那么只需要对所有区间进行排序再一步步取过来,如果出现目前合并的最大R到下一个区间的L之间有一段更新距离,则找到解。
之后的问题就是少量的分类讨论了。详情可以见代码。
#include <bits/stdc++.h>
#define M 500005
using namespace std;
#define Pair pair<int,int>
#define fi first
#define se second
vector<int>A[M];
int main(){
int n,c;
scanf("%d %d",&n,&c);
for(int i=1,sz;i<=n;i++){
scanf("%d",&sz);
for(int j=1,x;j<=sz;j++){
scanf("%d",&x);
A[i].push_back(x);
}
}
vector<Pair>Ans;
Ans.push_back((Pair){c,c});//[0,c-1]
for(int i=1;i<n;i++){
int tot=0;
while(tot<A[i].size()&&tot<A[i+1].size()&&A[i][tot]==A[i+1][tot])
++tot;
if(tot>=A[i].size()||tot>=A[i+1].size()){
//前缀部分相同,判断后缀部分
if(A[i].size()>A[i+1].size()){
puts("-1");
return 0;
}
}else{//停在相同的部分
if(A[i][tot]<A[i+1][tot]){
Ans.push_back((Pair){c-A[i+1][tot]+1,c-A[i][tot]});
}else{
Ans.push_back((Pair){0,c-A[i][tot]});
Ans.push_back((Pair){c-A[i+1][tot]+1,c-1});
}
}
}
sort(Ans.begin(),Ans.end());
int pre=-1;
for(int j=0;j<Ans.size();j++){
int L=Ans[j].fi,R=Ans[j].se;
if(pre+1<L){
printf("%d\n",pre+1);
return 0;
}else{
pre=max(pre,R);
}
}
if(pre+1<c)printf("%d\n",pre+1);
else puts("-1");
return 0;
}
E. Funny Game
题目大意:
Petya and Gena依次按照如下规则取:
- 选择序列左侧的最少两个元素,取走。然后在最左边放上与这些元素的总和相等的新元素,此时当前玩家本轮所得分数为这个总和。
Petra先取,要求得到Petra总分-Gena总分的最大值。
一类非常常见的带博弈色彩的dp。本题与POI2010_The_Minima_Game几乎一样。
定义 dp[i] 表示当前玩家取前缀和[1,i]的所有权值,相对于另外一个玩家的最优解。按照两玩家的游戏顺序,显然是从左往右依次取过来。那么 dp[i] 从后面另一个玩家的最大的最优解更新过来。原因略。
所以根据上述内容,我们有直观的记忆化搜索 O(N2) 转移:
#include <bits/stdc++.h>
#define M 200005
using namespace std;
int n,sum[M],dp[M];
int dfs(int pos){
int &tmp=dp[pos];
if(~tmp)return tmp;
tmp=-0x3f3f3f3f;
for(int j=pos+1;j<=n;j++)
tmp=max(tmp,dfs(j));
if(tmp==-0x3f3f3f3f)tmp=0;
return tmp=sum[pos]-tmp;
}
int main(){
scanf("%d",&n);
memset(dp,-1,sizeof(dp));
for(int i=1;i<=n;i++){
scanf("%d",&sum[i]);
sum[i]+=sum[i-1];
}
int ans=-0x3f3f3f3f;
for(int i=2;i<=n;i++)
ans=max(ans,dfs(i));
cout<<ans<<endl;
}
显然如果从大到小进行更新,那么由于当前解可以从[i+1,n]转移过来,那么这个最大的最优解可以直接维护。因此我们有以下 O(N) 的线性dp做法:
#include <bits/stdc++.h>
#define M 200005
using namespace std;
int sum[M];
int main(){
int n;scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&sum[i]);
sum[i]+=sum[i-1];
}
int ans=-0x3f3f3f3f;
for(int i=n;i>=2;i--)
if(ans==-0x3f3f3f3f)ans=sum[i];
else if(ans<sum[i]-ans)ans=sum[i]-ans;
cout<<ans<<endl;
}
F. Video Cards
题目大意:
在原序列中选择一个基底,再将原序列的其他数都改成最大的、不大于原数的、是该基底倍数的数(该倍数也可以是0),最后统计这些数的总和。求可能达到的最大总和。
本题我们考虑如下的暴力算法:
- 枚举每个数作为基底,再按照不同倍数级分别讨论 [Base∗k,Base∗(k+1)−1] 这段区间内元素应该如何处置。
实际上上述“暴力”算法就是正解。原因和POI2010_Beads一样,都是调和级数的问题:
接下来就是一些小优化了,譬如说排序离散,删去所有是该基底倍数的数作为基底的资格,或者改成静态前缀和(本来就该是静态的,但是我当时没有进一步思考结果用了树状数组),于是最终复杂度是非常松的 O(nlogn) :
#include <bits/stdc++.h>
#define M 200005
using namespace std;
int bit[M],res[M];
int main(){
int n;scanf("%d",&n);
long long sum=0;
for(int i=1;i<=n;i++){
scanf("%d",&res[i]);
bit[res[i]]++;
}
for(int i=1;i<M;i++)bit[i]+=bit[i-1];
long long ans=0;
sort(res+1,res+n+1);
n=unique(res+1,res+n+1)-(res+1);
for(int i=1;i<=n;i++){
int step=res[i];
long long sum=0;
for(int k=0,cnt;k*step<M;k++){//[step*k,step*(k+1)-1]
if(!k)cnt=bit[min(M-1,step*(k+1)-1)];
else cnt=bit[min(M-1,step*(k+1)-1)]-bit[step*k-1];
sum+=1ll*cnt*step*k;
}
if(ans<sum)ans=sum;
}
cout<<ans<<endl;
}