【AtCoder】AtCoder Grand Contest 025 解题报告(A~E)
\(A\):Digits Sum(点此看题面)
- 定义\(s(x)\)为十进制数\(x\)的各位数码和。
- 给出一个数\(n\),求\(\min_{i=1}^{n-1}(s(i)+s(n-i))\)。
- \(n\le10^5\)
签到题
暴枚即可。
代码:\(O(n\lg n)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
using namespace std;
int n;I int S(RI x) {RI t=0;W(x) t+=x%10,x/=10;return t;}//求数码和
int main()
{
RI i,t=1e9;for(scanf("%d",&n),i=1;i^n;++i) t=min(t,S(i)+S(n-i));//暴枚
return printf("%d\n",t),0;
}
\(B\):RGB Coloring(点此看题面)
- 有一个长度为\(n\)的序列,给定两个数\(A,B\)。
- 你可以在序列每个位置上填入\(A,B,A+B,0\)中的一个。
- 求使得序列总和为\(k\)的方案数。
- \(n,A,B\le3\times10^5,K\le18\times{10^{10}}\)
组合数学
考虑我们枚举填了\(i\)个\(A\),然后判一下\(j=\frac{k-iA}B\)是否是一个在\([0,n]\)范围内的整数,就可以得到所有使得\(iA+jB=k\)的方案。
对于一种方案,发现放\(A\)和放\(B\)是独立的,因此对答案的贡献就是\(C_n^i\times C_n^j\)。
代码:\(O(n)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define LL long long
#define N 300000
#define X 998244353
#define C(x,y) (1LL*Fac[x]*IFac[y]%X*IFac[(x)-(y)]%X)
using namespace std;
int n,A,B,pw[N+5],Fac[N+5],IFac[N+5];LL k;
I int QP(RI x,RI y) {RI t=1;W(y) y&1&&(t=1LL*t*x%X),x=1LL*x*x%X,y>>=1;return t;}
int main()
{
RI i,j,t=0;scanf("%d%d%d%lld",&n,&A,&B,&k);
for(Fac[0]=i=1;i<=n;++i) Fac[i]=1LL*Fac[i-1]*i%X;//预处理阶乘
for(IFac[n]=QP(Fac[n],X-2),i=n-1;~i;--i) IFac[i]=1LL*IFac[i+1]*(i+1)%X;//预处理阶乘逆元
for(i=0;i<=n&&1LL*i*A<=k;++i)//枚举放i个A
{
if((k-1LL*i*A)%B||(k-1LL*i*A)/B>n) continue;j=(k-1LL*i*A)/B;//找到一组方案
t=(1LL*C(n,i)*C(n,j)+t)%X;//求出对答案的贡献
}return printf("%d\n",t),0;
}
\(C\):Interval Game(点此看题面)
- 有一个数轴,给定\(n\)个区间。
- 现在甲、乙两人要进行一个\(n\)轮的游戏,初始时乙在原点。对于第\(i\)轮:
- 甲选择\(n\)个区间中一个尚未选过的区间。
- 乙走到该区间中任意一个点。
- 甲想要最大化乙的路程,乙想要最小化乙的路程,求乙最终的路程。
- \(n\le10^5\)
贪心
首先对于乙,容易发现局部最优解就是全局最优解,因此每次直接贪心走到当前区间离他最近的点就好了。
然后对于甲,肯定尽可能让乙左右横跳,因此轮流选择剩余区间中左端点最大、右端点最小的区间(或轮流选择右端点最小、左端点最大)即可。
代码:\(O(nlogn)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100000
#define LL long long
using namespace std;
int n,l[N+5],r[N+5],a[N+5],b[N+5],vis[N+5];
I bool cmp1(CI x,CI y) {return l[x]>l[y];}I bool cmp2(CI x,CI y) {return r[x]<r[y];}
I int Dis(int& x,CI p)//求出点x到区间p的距离,同时移动x
{
RI t;if(l[p]<=x&&x<=r[p]) return 0;//若x在区间中,不动
if(x>r[p]) return t=x-r[p],x=r[p],t;return t=l[p]-x,x=l[p],t;//移到较近的端点
}
int main()
{
RI i,x,p,q;for(scanf("%d",&n),i=1;i<=n;++i) scanf("%d%d",l+i,r+i);
for(i=1;i<=n;++i) a[i]=b[i]=i;sort(a+1,a+n+1,cmp1),sort(b+1,b+n+1,cmp2);
LL t1=0;for(x=0,p=q=i=1;i<=n;++i)//轮流选择左端点较大、右端点较小
{
W(p<=n&&vis[a[p]]) ++p;if(p>n) break;t1+=Dis(x,a[p]),vis[a[p]]=1;
W(q<=n&&vis[b[q]]) ++q;if(q>n) break;t1+=Dis(x,b[q]),vis[b[q]]=1;
}t1+=abs(x);
LL t2=0;for(x=0,p=q=i=1;i<=n;++i)//轮流选择右端点较小、左端点较大
{
W(q<=n&&!vis[b[q]]) ++q;if(q>n) break;t2+=Dis(x,b[q]),vis[b[q]]=0;
W(p<=n&&!vis[a[p]]) ++p;if(p>n) break;t2+=Dis(x,a[p]),vis[a[p]]=0;
}t2+=abs(x);
return printf("%lld\n",max(t1,t2)),0;//两种情况取较大值
}
\(D\):Choosing Points(点此看题面)
- 有一张\(2n\times 2n\)的网格图,给定\(D_1,D_2\)
- 要求构造一种方案,从中选出\(n^2\)个点,满足任意两点距离既不是\(\sqrt{D_1}\)也不是\(\sqrt{D_2}\)。
- \(n\le300\)
平方和模\(4\)的余数
众所周知,一个数的平方模\(4\)意义下有一些性质。
对于一个奇数,\((2k+1)^2=4(k^2+k)+1\equiv1(mod\ 4)\)。
对于一个偶数,\((2k)^2=4k^2\equiv0(mod\ 4)\)。
所以对于某一个\(D\),若它是两个数的平方和,可以根据其模\(4\)的余数讨论。
注意,两数平方和模\(4\)显然不可能余\(3\),因此这种情况直接忽略。
\(D\equiv1(mod\ 4)\)
\(D\)必然为一个奇数和一个偶数的平方和。
则我们可以采取国际棋盘的选数方式,显然任意两点横纵距离奇偶性相同。
例如:
\(D\equiv2(mod\ 4)\)
\(D\)必然为两个奇数的平方和。
则我们可以采取隔行的选数方式,显然两点竖直距离必然为偶数。
例如:
\(D\equiv0(mod\ 4)\)
\(D\)必然为两个偶数的平方和。
设\(D=x^2+y^2\),则\(\frac{D}4=(\frac x2)^2+(\frac y2)^2\)。
所以我们不断将\(D\)除以\(4\)直至它不再是\(4\)的倍数,与此同时我们将每个单位的长宽分别变为原来的\(2\)倍,然后按上面两种情况做。
说起来可能有点抽象,例如\(D=8\):(此时是以每个\(2\times2\)的正方形为一个单位)
代码:\(O(n^2)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 300
using namespace std;
int n,nn,d1,d2,a[2*N+5][2*N+5];struct Data {int x,y;}s[N*N+5];
I void Draw(RI x)
{
RI i,j,t=0;W(!(x%4)) x/=4,++t;//若是4的倍数,不断除以4,同时扩大单位长度
if(x%4==1) for(i=0;i^nn;++i) for(j=0;j^nn;++j) a[i][j]|=((i>>t)^(j>>t))&1;//棋盘
if(x%4==2) for(i=0;i^nn;++i) for(j=0;j^nn;++j) a[i][j]|=(i>>t)&1;//隔行
}
int main()
{
RI i,j,t=0;scanf("%d%d%d",&n,&d1,&d2),nn=n<<1;Draw(d1),Draw(d2);
for(i=0;i^nn;++i) for(j=0;j^nn;++j) !a[i][j]&&t^(n*n)&&(printf("%d %d\n",i,j),++t);return 0;//输出前n^2个没打过标记的点即可
}
\(E\):Walking on a Tree(点此看题面)
- 给定一棵\(n\)个点的树以及\(m\)条树上路径。
- 你需要给这\(m\)条树上路径定向,并定义每条树边的权值是经过它的路径的方向数。
- 求最大权值总和,并给出任意一种定向方案。
- \(n,m\le2000\)
题解
考虑我们从叶节点开始往上搞,并分类讨论:
-
如果一个叶节点没有经过它的路径,直接忽略。
-
如果一个叶节点有一条路径,那么无论这条路径如何定向,这个叶节点到其父节点的路径贡献都是\(1\)。那么可以直接把这条路径送给父节点,对答案没有影响。
-
如果一个叶节点有大于一条路径,我们可以从中任选两条,让它们方向不同,贡献就可以取到\(2\)。
- 假设叶节点为\(k\),两条路径另一端点分别为\(a,b\),则\(k\)到\(a,b\)路径交集中的所有边贡献都成了\(2\)。
- 而对于其他边,只要保留下一条端点为\(a,b\)的新的路径,容易发现并不会对能取到的最大贡献造成影响。且\(k,a\)和\(k,b\)两条路径的方向完全取决于\(a,b\)这条路径的方向。
而这个叶节点其他边可以和上种情况一样直接送给父节点。
具体实现需要对每个点开个\(set\),然后就是模拟了。
代码:\(O(n(n+m)logm)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 2000
#define add(x,y) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y)
#define swap(x,y) (x^=y^=x^=y)
using namespace std;
int n,m,fa[N+5],dep[N+5],deg[N+5],tg[N+5],X[N+5],Y[N+5];
int ee,lnk[N+5];struct edge {int to,nxt;}e[N<<1];
struct Edge {int x,y,f,t;}s[N+5];set<int> S[N+5];set<int>::iterator it;
I void Init(CI x=1)//初始化,求出每个点的深度及其父节点
{
for(RI i=lnk[x];i;i=e[i].nxt) e[i].to^fa[x]&&
(dep[e[i].to]=dep[fa[e[i].to]=x]+1,Init(e[i].to),++deg[x]);
}
I int Mark(CI x,CI a,CI b,CI lst=0)//给x到a,b路径交集打标记
{
#define M(x,y) (dep[e[x].to]>dep[e[y].to]?tg[e[x].to]=1:tg[e[y].to]=1)//一条边的标记打在深度较大的端点上
RI t,res=(x==a)<<1|(x==b);
for(RI i=lnk[x];i;i=e[i].nxt) e[i].to^lst&&
((t=Mark(e[i].to,a,b,x))==3&&M(i,((i-1)^1)+1),res|=t);//如果得到3,说明这条边能到达a,b两点
return res;
}
int Q[N+5];I int Topo()//拓扑
{
RI i,k,H=1,T=0,res=0,p,q;for(i=1;i<=n;++i) !deg[i]&&(Q[++T]=i);W(Q[H]^1)//初始化拓扑序列
{
#define Swap(i) (swap(s[i].x,s[i].y),s[i].t^=1)//交换i号边的两个端点
#define Union(x,y) (s[y].t^=s[s[y].f=x].t)//y的方向取决于x,注意先异或上x已有的变化值
#define Trans(i,t) ((s[i].x=t)^s[i].y?(S[t].insert(i),0):(S[t].erase(i),0))//将i的左端点修改为t,注意判断当边缩成点时要删掉
k=Q[H++],!--deg[fa[k]]&&(Q[++T]=fa[k]),//拓扑
(tg[k]||S[k].size()>1)?res+=2:res+=S[k].size();//计算这条边的贡献
for(it=S[k].begin();it!=S[k].end();++it) s[*it].y==k&&Swap(*it);//方便起见,将所有边左端点改为当前点
!tg[k]&&S[k].size()>1&&//如果没打过标记且有超过一条边
(
p=*S[k].begin(),S[k].erase(S[k].begin()),q=*S[k].begin(),S[k].erase(S[k].begin()),//任取两条边
Mark(k,s[p].y,s[q].y),Union(p,q),s[q].t^=1,S[s[q].y].erase(q),Trans(p,s[q].y)//打标记,把p变成a,b,删去q且其方向取决于p
);
for(it=S[k].begin();it!=S[k].end();++it) Trans(*it,fa[k]);//剩下的边送给父节点
}return res;
}
int vis[N+5];I void F5(CI x) {s[x].f&&!vis[x]&&(F5(s[x].f),vis[x]=1,s[x].t^=s[s[x].f].t);}//求出每条边的最终方向
int main()
{
RI i,x,y;for(scanf("%d%d",&n,&m),i=1;i^n;++i) scanf("%d%d",&x,&y),add(x,y),add(y,x);Init();
for(i=1;i<=m;++i) scanf("%d%d",X+i,Y+i),S[s[i].x=X[i]].insert(i),S[s[i].y=Y[i]].insert(i);
for(printf("%d\n",Topo()),i=1;i<=m;++i) F5(i),//输出方案之前先更新信息
s[i].t&&swap(X[i],Y[i]),printf("%d %d\n",X[i],Y[i]);return 0;
}