今天下午看到有7个题,打了个vp,一开始准备用chenxiaoyan打的,然后调错时间了,无奈只能用小号WinnieThePooh打/xk
最终还剩几分钟的时候AK了/cy
吐槽一下,这场咋全多测啊?
CF比赛页面传送门
A - Three Pairwise Maximums
洛谷还没爬,下同,管理员快爪巴 & CF题目页面传送门
题意紫帆。
考虑将(a,b,c)从小到大排序,则(x=b,y=z=c)。将题目给出的(x,y,z)排序之后,若(y eq z)则无解,否则(a=1,b=x,c=y)是一组解。
代码:
#include<bits/stdc++.h>
using namespace std;
void mian(){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
if(x>y)swap(x,y);if(y>z)swap(y,z);
if(x>y)swap(x,y);if(y>z)swap(y,z);
if(y!=z)return puts("NO"),void();
puts("YES");
cout<<1<<" "<<x<<" "<<y<<"
";
}
int main(){
int testnum;
cin>>testnum;
while(testnum--)mian();
return 0;
}
B - Restore the Permutation by Merger
CF题目页面传送门
题意紫帆。
把(1sim n)所有数以第一次出现的位置取下来,相对位置不变,组成一个排列,就是答案。自证不难。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=50;
int n;
bool hav[N+1];
void mian(){
cin>>n;
memset(hav,0,sizeof(hav));
for(int i=1;i<=2*n;i++){
int x;
scanf("%d",&x);
if(!hav[x])hav[x]=true,printf("%d ",x);
}
puts("");
}
int main(){
int testnum;
cin>>testnum;
while(testnum--)mian();
return 0;
}
C - Make It Good
CF题目页面传送门
题意紫帆。
不难发现一个数列是好的当且仅当它非严格单峰。自证不难。又发现,删掉前缀相当于留下后缀(这个出题人迷惑的太失败了),右端点是不变的。于是贪心,从右往左找到最左边的一个可以当峰值的位置,然后再从峰值往左边找到最左边的左端点。最终答案就是左端点减一。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=200000;
int n;
int a[N+1];
void mian(){
cin>>n;
for(int i=1;i<=n;i++)scanf("%d",a+i);
int las;
for(int i=n;i;i--){//找最左峰值
if(i<n&&a[i]<a[i+1])break;
las=i;
}
int las0;
for(int i=las;i;i--){//找最左左端点
if(i<las&&a[i]>a[i+1])break;
las0=i;
}
cout<<las0-1<<"
";
}
int main(){
int testnum;
cin>>testnum;
while(testnum--)mian();
return 0;
}
D - a-Good String
CF题目页面传送门
题意紫帆。
做法很显然,考虑DP,设(dp_{l,r,x})表示子串(s_{lsim r})成为(x)-good string所需要的最小操作数。边界:(dp_{l,l,x}=[s_l eq x]),目标:(dp_{1,n, exttt a}),转移:(dp_{l,r,x}=minleft(sumlimits_{i=l}^{frac{l+r-1}2}[s_i eq x]+dp_{frac{l+r-1}2+1,r,x+1},sumlimits_{i=frac{l+r-1}2+1}^{r}[s_i eq x]+dp_{l,frac{l+r-1}2,x+1} ight))。
然而这样看起来空间是(mathrm O!left(n^2log n ight))的,其实合法的((l,r))对只有(mathrm O(n))个,跟线段树类似,而且对于每个合法的((l,r))都只有一个合法的(x)对应。所以空间复杂度(mathrm O(n)),时间复杂度类似归并树,是(mathrm O!left(sum nlog n ight))的。然鹅现场我没有考虑到,于是空间开了(mathrm O(nlog n)),清空数组的时候也花了这么多。不管了。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=200000,LET=20;
int n;
char a[N+5];
int dp[N<<2][LET];
int dfs(int l=1,int r=n,int c=0,int p=1){//记忆化搜索
if(l==r)return a[l]!='a'+c;
if(~dp[p][c])return dp[p][c];
int &res=dp[p][c],mid=l+r>>1,sum1=0,sum2=0;
for(int i=l;i<=mid;i++)sum1+=a[i]!='a'+c;
for(int i=mid+1;i<=r;i++)sum2+=a[i]!='a'+c;
res=min(sum1+dfs(mid+1,r,c+1,p<<1|1),sum2+dfs(l,mid,c+1,p<<1));//转移方程
// printf("dp[%d][%d][%d]=%d
",l,r,c,res);
return res;
}
void mian(){
cin>>n;
scanf("%s",a+1);
for(int i=1;i<=4*n;i++)for(int j=0;j<LET;j++)dp[i][j]=-1;//清空
cout<<dfs()<<"
";
}
int main(){
int testnum;
cin>>testnum;
while(testnum--)mian();
return 0;
}
E - Directing Edges
CF题目页面传送门
有一张(n)个点(m)条边的混合图,你需要给所有无向边定向,使得最终得到的有向图无环。或报告无解。本题多测。
(sum ninleft[2,2 imes10^5 ight],sum minleft[1,2 imes10^5 ight])。
事实证明,这是本场最难的一题,因为我到最后才想出来
首先注意到,若不考虑所有无向边,剩下来的有向图是有环的,那显然无解。
然后呢?当时顺序做到这题的时候我想了DFS,Tarjan缩点(这个一年没写了,我已经不会了),都没有思路,却没有考虑到有向图上经常用到的拓扑排序(最后才想到)。考虑对不考虑所有无向边得到的有向图拓扑排序,那么无向边的定向方案很容易构造:拓扑序小的连向拓扑序大的即可保证无环。
代码:
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define mp make_pair
#define X first
#define Y second
const int N=200000;
int n,m;
vector<pair<int,int> > nei[N+1];
int ideg[N+1];
vector<int> topo;
int id[N+1];
void toposort(){//拓扑排序
topo.clear();
queue<int> q;
for(int i=1;i<=n;i++)if(!ideg[i])q.push(i);
while(q.size()){
int x=q.front();
q.pop();
topo.pb(x);
for(int i=0;i<nei[x].size();i++){
int y=nei[x][i].X,z=nei[x][i].Y;
if(z)/*不考虑无向边*/if(!--ideg[y])q.push(y);
}
}
}
void mian(){
cin>>n>>m;
for(int i=1;i<=n;i++)ideg[i]=0,nei[i].clear();
while(m--){
int x,y,z;
scanf("%d%d%d",&z,&x,&y);
if(z)nei[x].pb(mp(y,z)),ideg[y]++;
else nei[x].pb(mp(y,z)),nei[y].pb(mp(x,z));
}
toposort();
if(topo.size()!=n)return puts("NO"),void();//无解
puts("YES");
for(int i=0;i<n;i++)id[topo[i]]=i;
for(int i=1;i<=n;i++)for(int j=0;j<nei[i].size();j++){
int x=nei[i][j].X,y=nei[i][j].Y;
if(y)printf("%d %d
",i,x);
else if(id[i]<id[x])printf("%d %d
",i,x);//拓扑序小的连向拓扑序大的
}
}
int main(){
int testnum;
cin>>testnum;
while(testnum--)mian();
return 0;
}
F - Removing Leaves
CF题目页面传送门
给定一棵无根树(T=(V,E),|V|=n,|E|=n-1)和一个常数(m)。每次可以选择恰好(m)个唯一连接的节点相同的叶子并删除。求最多能删多少次。本题多测。
(sum ninleft[1,2 imes 10^5 ight])。
我记得我在这篇里吐槽过打的3次D3F全是树形DP?这次树形DP喜加一。
注意到,对于任何一种删的方案,最终必定会有至少一个节点留下来删不掉。我们可以钦定这个点为根树形DP,最后二次扫描。接下来考虑如何DP。
设(dp0_i)表示子树(i)是否能删得只剩一个(i),(dp_i)表示子树(i)内最多能删几次。转移挺简单的,设(cnt_i=sumlimits_{jin son_i}dp0_j),则
接下来就愉快地做出来了。
然而现场我nt了。我就懒得写严格(mathrm O(1))的换根,写了个calc
函数计算DP值((mathrm O(|son_x|))),然后换根的时候调用这个函数。我当时zz地认为这样是均摊(mathrm O(1))的,交上去,TLE13。我就很生气,CF啥时候也卡常了?就算卡,这也要卡?于是吸臭氧码读优还是T。无奈之下只好改成严格(mathrm O(1))的换根,这样还需要记录(cnt)数组。然后就A了。赛后才发现一个菊花图就能把我卡没了。。。。。
代码:
#pragma GCC optimize(3)///xk
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
void read(int &x){///xk
x=0;char c=getchar();
while(!isdigit(c))c=getchar();
while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
}
void prt(int x){///xk
if(x>9)prt(x/10);
putchar(x%10^48);
}
const int N=200000;
int n,m;
vector<int> nei[N+1];
int cnt[N+1];
bool dp0[N+1];
int dp[N+1];
void dfs(int x=1,int fa=0){//初DP
cnt[x]=0;dp0[x]=true;dp[x]=0;
for(int i=0;i<nei[x].size();i++){
int y=nei[x][i];
if(y==fa)continue;
dfs(y,x);
cnt[x]+=dp0[y];dp0[x]&=dp0[y];dp[x]+=dp[y];
}
dp[x]+=cnt[x]/m;
dp0[x]&=cnt[x]%m==0;
// printf("dp[%d]=%d
",x,dp[x]);
}
int ans;
void dfs0(int x=1,int fa=0){//二次扫描
// printf("%d=%d
",x,dp[x]);
ans=max(ans,dp[x]);//更新答案
for(int i=0;i<nei[x].size();i++){
int y=nei[x][i];
if(y==fa)continue;
int cnt_x=cnt[x],dp0_x=dp0[x],dp_x=dp[x],cnt_y=cnt[y],dp0_y=dp0[y],dp_y=dp[y];
cnt[x]-=dp0[y];dp0[x]=cnt[x]==nei[x].size()-1&&cnt[x]%m==0;dp[x]=dp[x]-dp[y]-(cnt[x]+dp0[y])/m+cnt[x]/m;
cnt[y]+=dp0[x];dp0[y]=cnt[y]==nei[y].size()&&cnt[y]%m==0;dp[y]=dp[y]+dp[x]-(cnt[y]-dp0[x])/m+cnt[y]/m;//换根
dfs0(y,x);
cnt[x]=cnt_x;dp0[x]=dp0_x;dp[x]=dp_x;cnt[y]=cnt_y;dp0[y]=dp0_y;dp[y]=dp_y;//还原
}
}
void mian(){
read(n);read(m);
for(int i=1;i<=n;i++)nei[i].clear();
for(int i=1;i<n;i++){
int x,y;
read(x);read(y);
nei[x].pb(y);nei[y].pb(x);
}
dfs();
ans=0;dfs0();
prt(ans);putchar('
');
}
int main(){
int testnum;
cin>>testnum;
while(testnum--)mian();
return 0;
}
G - Columns Swaps
CF题目页面传送门
有一个(2 imes n)的矩阵(a),每个数在([1,n])内。要求若干次交换某一列的(2)个值,使得最后每行都是(1sim n)的排列。求最小次数以及对应方案,或报告无解。本题多测。
(sum nin[1,2 imes 10^5])。
首先,每个数出现的次数必须要是(2),否则无解。
然后。看到排列想到图论。不难发现结论:若(forall iin[1,n]),连有向边((a_{1,i},a_{2,i})),得到的图可以表示为一个排列当且仅当上下都是排列。证明的话,必要性可以用置换的乘积证,充分性xjb随便证即可(就是每个点入出度都为(1),则在上下各出现过一次)。
那么,原操作相当于将一条边反向。考虑将原(a)的图建出来,不考虑方向依次考虑每个CC(环),然后可以整体调成(2)种方向,比个大小即可。对于CC大小为(1)和(2)需要讨论一下,有点烦?(当时差点讨论绝望了)
代码:
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define mp make_pair
#define X first
#define Y second
const int N=200000;
int n;
int a[N+1],b[N+1];
vector<pair<int,pair<int,int> > > nei[N+1];
int cnt[N+1];
bool vis[N+1];
vector<int> zero,one;
int st,to;
void dfs(int x){//找环
vis[x]=true;
for(int i=0;i<nei[x].size();i++){
int y=nei[x][i].X,z=nei[x][i].Y.X,xx=nei[x][i].Y.Y;
if(!vis[y]){
if(x==st)to=y;
dfs(y);
if(xx)one.pb(z);else zero.pb(z);
}
}
}
void mian(){
scanf("%d",&n);
for(int i=1;i<=n;i++)cnt[i]=0,nei[i].clear(),vis[i]=false;
for(int i=1;i<=n;i++)scanf("%d",a+i),cnt[a[i]]++;
for(int i=1;i<=n;i++)scanf("%d",b+i),cnt[b[i]]++;
for(int i=1;i<=n;i++)if(cnt[i]!=2)return puts("-1"),void();//判无解
for(int i=1;i<=n;i++)nei[a[i]].pb(mp(b[i],mp(i,1))),nei[b[i]].pb(mp(a[i],mp(i,0)));//建图
vector<int> ans;
for(int i=1;i<=n;i++)if(!vis[i]){
one.clear(),zero.clear(),st=i,dfs(i);
if(one.empty()&&zero.empty())continue;//大小为1
if(nei[st][0].X==nei[st][1].X){//大小为2
if(nei[st][0].Y.Y==nei[st][1].Y.Y)ans.pb(nei[st][0].Y.X);
continue;
}
int id=nei[st][0].X==to?1:0;
if(nei[st][id].Y.Y)zero.pb(nei[st][id].Y.X);else one.pb(nei[st][id].Y.X);
if(one.size()<zero.size())for(int i=0;i<one.size();i++)ans.pb(one[i]);
else for(int i=0;i<zero.size();i++)ans.pb(zero[i]);//比大小压进答案序列
}
printf("%d
",int(ans.size()));
for(int i=0;i<ans.size();i++)printf("%d ",ans[i]);
puts("");
}
int main(){
int testnum;
cin>>testnum;
while(testnum--)mian();
return 0;
}