如无特殊说明,下文中的 \(T\) 代指有多组数据题目的数据组数。
Problem A
题意
给定一个长度为 \(n\) 的整数数组 \(a\),每次可以从中选出若干个 \(a_{p_1},a_{p_2},\cdots,a_{p_k}\)。记它们的平均数为 \(d\),那么将所有满足 \(1 \leq i \leq k\) 且 \(a_{p_i} > d\) 的 \(a_{p_i}\) 删除。问最多可以删除多少个数。
对于任意 \(m\) 个数 \(b_1,b_2,\cdots,b_m\),它们的平均数为 \(\dfrac 1m \times \sum \limits_{i=1}^m b_i\)。
\(1 \leq T,n,a_i \leq 100\)
题解
选多个没什么用,不如就每次选两个。
观察到每次可以用一个小的数去删一个大的数,那么最后只有最小值无法被删除。
于是答案就是 \(n\) 减去最小值个数。
Problem B
题意
给定一个长度为 \(n\) 的整数数组 \(a\),求出 \(a\) 的最长子序列 \(b_{1 \cdots m}\) 使得对于任意 \(1 \leq i < j \leq m\) 都有 \(|b_i - b_j| \geq \max\{b_i\}\)。
\(1 \leq T \leq 10^4,1 \leq n \leq 10^5,-10^9 \leq a_i \leq 10^9\)
题解
观察到只包含小于等于 \(0\) 的数的子序列一定满足条件。同时,如果有一个子序列包含了至少两个正数,那么一定不满足条件。
先把所有小于等于 \(0\) 的 \(a_i\) 找出来作为 \(b\),再来考虑能不能加入恰好一个正数作为新序列 \(b\) 的最大值。设我们要加入的正数为 \(c\),那么 \(|c - b_i| \geq c\) 一定成立。我们只需要考虑 \(|b_i - b_j| \geq c\) 是否一定成立。那么 \(c\) 只需要小于等于 \(\min\{|b_i-b_j|\}\) 即可,否则找不到这样的 \(c\)。
# include <bits/stdc++.h>
const int N=100010,INF=0x3f3f3f3f;
int a[N];
inline int read(void){
int res,f=1;
char c;
while((c=getchar())<'0'||c>'9')
if(c=='-')f=-1;
res=c-48;
while((c=getchar())>='0'&&c<='9')
res=res*10+c-48;
return res*f;
}
int main(void){
int T=read();
while(T--){
int n=read(),mzcnt=0,pcnt=0;
for(int i=1;i<=n;++i){
a[i]=read();
if(a[i]<=0)
++mzcnt;
if(a[i]>0)
++pcnt;
}
std::sort(a+1,a+1+n);
int maxsize=INF;
for(int i=1;i<n;++i){
if(a[i]<=0&&a[i+1]<=0){
maxsize=std::min(maxsize,a[i+1]-a[i]);
}
}
int ans=mzcnt;
for(int i=1;i<=n;++i){
if(a[i]>0&&a[i]<=maxsize){
++ans;
break;
}
}
printf("%d\n",ans);
}
return 0;
}
Problem C
题意
给定 \(n\) 个节点的树,第 \(i\) 个节点上有一个区间 \([l_i,r_i]\)。你需要给第 \(i\) 个节点分配一个正整数 \(a_i\),且满足 \(1 \leq a_i \leq r_i\)。一条树边 \((u,v)\) 的价值为 \(|a_u - a_v|\)。求树边之和的最大值。
\(1 \leq T \leq 250,1 \leq n \leq 10^5,1 \leq l_i,r_i \leq 10^9\)
题解
手玩一下发现,每个点要么选 \(l_i\) 要么选 \(r_i\)。然而我不会证明。
于是就是一个普通树形 dp。
# include <bits/stdc++.h>
# define int long long
const int N=200010,INF=0x3f3f3f3f;
std::vector <int> G[N];
int n;
int l[N],r[N],dp[N][2];
inline int read(void){
int res,f=1;
char c;
while((c=getchar())<'0'||c>'9')
if(c=='-')f=-1;
res=c-48;
while((c=getchar())>='0'&&c<='9')
res=res*10+c-48;
return res*f;
}
inline int mabs(int x){
return x>0?x:-x;
}
void dfs(int i,int fa){
for(int j=0;j<(int)G[i].size();++j){
int to=G[i][j];
if(to==fa)
continue;
dfs(to,i);
dp[i][0]+=std::max(dp[to][0]+mabs(l[i]-l[to]),dp[to][1]+mabs(l[i]-r[to]));
dp[i][1]+=std::max(dp[to][0]+mabs(r[i]-l[to]),dp[to][1]+mabs(r[i]-r[to]));
}
return;
}
signed main(void){
int T=read();
while(T--){
n=read();
for(int i=1;i<=n;++i){
l[i]=read(),r[i]=read(),dp[i][0]=dp[i][1]=0;
G[i].clear();
}
for(int i=1;i<n;++i){
int u=read(),v=read();
G[u].push_back(v),G[v].push_back(u);
}
dfs(1,0);
printf("%lld\n",std::max(dp[1][0],dp[1][1]));
}
return 0;
}
Problem D
题意
一条数轴上有 \(2n\) 个点,第 \(i\) 个点坐标为 \(i\)。\(2n\) 个点将被分成 \(n\) 组,每组之间连线。
问有多少种方案 \((l_1,r_1),(l_2,r_2),\cdots,(l_n,r_n)\)(其中 \(l_i<r_i\)), 使得对于任意 \(i,j\),以下两个条件至少满足一个:
- 第 \(i\) 组和第 \(j\) 组的连线长度相等,即 \(r_i - l_i = r_j - l_j\)
- 其中一组的连线被另外一组完全包含,即 \(l_i \leq l_j \leq r_j \leq r_i\) 或 \(l_j \leq l_i \leq r_i \leq r_j\)
例如当 \(n=2\) 时,答案为 \(3\),分别为 \(\{(1,2),(3,4)\},\{(1,3),(2,4)\}\) 和 \(\{(1,4),(2,3)\}\)。
\(1 \leq n \leq 10^6\)
题解
考虑递推。设 \(f_i\) 表示 \(n=i\) 时的答案。显然有 \(f_0=f_1=1\)。
对于 \(i>1\) 的情况,考虑与 \(1\) 连线的点的位置 \(p\)。当 \(p > n\) 时,以在 \(p\) 右侧的点为端点的连线无法被连线 \((1,p)\) 完全包含,因此这些点只能连长度为 \(p-1\) 的线。设 \(x=2n-p\),算上 \(1\) 和 \(p\),一共有 \(2(x+1)\) 个点的连线完全固定。
剩下的点即 \([x+2,2n-x-1]\),这些点之间的连线一定会被这些长度为 \(2n-x-1\) 的线包含,于是只需要考虑这些连线之间是否合法,方案是一个规模为 \(n-2(x+1)\) 的子问题的答案,即 \(f_{n-2(x+1)}\)。
当 \(p \leq n\) 时,在 \(p\) 右侧的点同样只能连长度为 \(p-1\) 的线。而 \([2,p-1]\) 中的点如果相互连线,那么必然不合法(连线的长度 \(<p-1\),但右侧一定有一条长度为 \(p-1\) 的线),所以它们也只能连长度为 \(p-1\) 的线。
注意到 \([1,p-1]\) 中的点 \(s\) 要向点 \(s+p-1\) 连线,而点 \([p,2p-2]\) 中的所有点不用连线,以此类推。可以看出连线在连续的 \(2p-2\) 个点内部,于是只要满足 \((2p-2) \mid 2n \Leftrightarrow (p-1)\mid n\),线就可以连完。统计 \(n\) 非本身因子个数即可。
综上可得,\(f_i = \sum \limits_{j=0}^{i-1} f_j+\sum \limits_{d=1}^{i-1} [d\mid n]\)。
时间复杂度 \(O(n \log n)\) 或 \(O(n)\)(使用线性筛预处理约数个数函数)。
# include <bits/stdc++.h>
const int N=1000010,INF=0x3f3f3f3f,mod=998244353;
int f[N],sum[N];
inline int read(void){
int res,f=1;
char c;
while((c=getchar())<'0'||c>'9')
if(c=='-')f=-1;
res=c-48;
while((c=getchar())>='0'&&c<='9')
res=res*10+c-48;
return res*f;
}
inline void add(int &x,int v){
x+=v;
if(x>=mod)
x-=mod;
return;
}
int main(void){
int n=read();
f[0]=1,sum[0]=1;
for(int i=1;i<=n;++i){
add(f[i],sum[i-1]);
sum[i]=sum[i-1],add(sum[i],f[i]);
for(int j=2*i;j<=n;j+=i){
++f[j];
}
}
printf("%d",f[n]);
return 0;
}
Problem E
题意
给定两棵 \(n\) 个点的有根树 \(A,B\),根为 \(1\)。求出一个最大的点集 \(S\),使得 \(\forall a,b \in S\),\(a,b\) 在 \(A\) 上有祖先关系,在 \(B\) 上没有祖先关系。
\(1 \leq T \leq 3 \times 10^5\),\(1 \leq n \leq 3 \times 10^5\),保证每个点 \(i\) 的父亲编号 \(<i\)
题解
容易证明,集合 \(S\) 中的点一定都在 \(A\) 树从根开始的一条链上。可以使用 DFS + 回溯维护所有这样的链。接下来的问题是如何选取没有祖先关系的点。
考虑欧拉序。设一个节点的欧拉序分别为 \(in_i\) 和 \(out_i\)。容易发现,对于任意两个节点 \(i,j\),要么有 \([in_i,out_i]\) 和 \([in_j,out_j]\) 不交,要么一个区间被另外一个区间覆盖。前者对应着没有祖先关系的情况,后者反之。
我们有一个贪心策略。考虑当前的 \(S\) 中加入一个区间 \([l,r]\),答案会如何变化。
- 如果 \([l,r]\) 和 \(S\) 中任何区间不交,那么加入 \([l,r]\) 不会有任何冲突。
- 如果 \([l,r]\) 覆盖了 \(S\) 中的一个区间,加入 \([l,r]\) 而删除该区间一定不优(\([l,r]\) 可能覆盖接下来的其它区间)。
- 如果 \(S\) 中的一个区间覆盖了 \([l,r]\),那么删除该区间并加入 \([l,r]\) 一定不劣。
可以使用权值线段树或者 std::set
来对上述情况进行判断。
时间复杂度 \(O(n \log n)\)。
# include <bits/stdc++.h>
const int N=300010,INF=0x3f3f3f3f;
typedef std::pair <int,int> pii;
struct Edge{
int to,next;
};
struct Tree{
Edge edge[N];
int head[N],sum;
inline void reset(int n){
std::fill(head+1,head+1+n,0),sum=0;
return;
}
inline void add(int x,int y){
edge[++sum]=(Edge){y,head[x]},head[x]=sum;
return;
}
}A,B;
int n;
int idfn[N],odfn[N],t;
int ans;
std::set <pii> S;
inline int read(void){
int res,f=1;
char c;
while((c=getchar())<'0'||c>'9')
if(c=='-')f=-1;
res=c-48;
while((c=getchar())>='0'&&c<='9')
res=res*10+c-48;
return res*f;
}
void dfsB(int i){
idfn[i]=++t;
for(int j=B.head[i];j;j=B.edge[j].next)
dfsB(B.edge[j].to);
odfn[i]=++t;
return;
}
inline int calc(int x){
pii now=std::make_pair(idfn[x],x);
std::set <pii>::iterator it=S.lower_bound(now);
if(it!=S.end()&&odfn[(*it).second]<=odfn[x]) // 左端点 >=l,右端点 <=r,那么 [l,r] 覆盖了一个区间
return 0;
if(it==S.begin())
return -1;
--it; // 找到左端点小于 l 的第一个区间,看它是否覆盖 [l,r]
if(odfn[(*it).second]>odfn[x]){
int val=(*it).second;
S.erase(it);
return val; // 记录下被删除的区间编号,方便回溯
}
return -1; // -1 代表 [l,r] 和 S 中区间不交
}
void dfsA(int i){
int res=calc(i);
if(res)
S.insert(std::make_pair(idfn[i],i)); // res 不为 0 时,代表着要加入该区间
ans=std::max(ans,(int)S.size());
for(int j=A.head[i];j;j=A.edge[j].next)
dfsA(B.edge[j].to);
if(res>0)
S.insert(std::make_pair(idfn[res],res)); // 回溯
S.erase(std::make_pair(idfn[i],i)); // 回溯
return;
}
int main(void){
int T=read();
while(T--){
n=read();
A.reset(n),B.reset(n);
for(int i=2;i<=n;++i)
A.add(read(),i);
for(int i=2;i<=n;++i)
B.add(read(),i);
t=0,ans=0;
dfsB(1);
dfsA(1);
printf("%d\n",ans);
}
return 0;
}
Problem F
题意
给定 \(n\) 个点 \(m\) 条边的带权有向图,点从 \(0\) 开始编号。保证每个点都是至少一条边的起点。
这张图很神奇,边的终点会变化。具体来讲,对于图中的一条边 \((a_i,b_i,c_i)\),它在第 \(x\) 秒的终点会变为 \((b_i + x) \bmod n\),即该边被替换为 \((a_i,(b_i+x) \bmod n,c_i)\) 这条边。你可以在一个点上等待任意时间。
通过一条边需要花费的时间等于它的权值。对于图中所有点对 \((u,v)\),求出 \(u\) 到 \(v\) 的最短时间。注意,当你从一条边出发时,你一定会到达你出发时该边的终点,尽管在你行走时该边的终点可能会发生变化。
\(1 \leq n \leq 600,n \leq m \leq n^2,0 \leq a_i,b_i \leq n-1,1 \leq c_i \leq 10^9\)
题解
朴素最短路算法唯一可能出错的情况就是你在某个点等待了一段时间。
为了解决这个问题,我们可以在图中增加一些「假边」。如果我们在节点 \(u\) 等了 \(1\) 秒才走 \((u,v,w)\) 这条边,就等同于先走到 \((v-1) \bmod n\),然后通过一条权值为 \(1\) 的「假边」\(((v-1) \bmod n,v,1)\) 到达 \(v\)。
因为 \(m\) 最大可能是 \(n^2\),所以需要使用朴素的 \(O(n^2)\) Dijkstra 而非堆优化。
代码做了一些小小的转化。
# include <bits/stdc++.h>
const int N=610,INF=0x3f3f3f3f;
typedef long long ll;
struct Edge{
int to,next,v;
}edge[N*N];
int head[N],sum;
ll dis[N],w[N];
int n,m;
bool c[N];
inline int read(void){
int res,f=1;
char c;
while((c=getchar())<'0'||c>'9')
if(c=='-')f=-1;
res=c-48;
while((c=getchar())>='0'&&c<='9')
res=res*10+c-48;
return res*f;
}
inline void add(int x,int y,int v){
edge[++sum]=(Edge){y,head[x],v},head[x]=sum;
return;
}
inline void Dijkstra(int s){
memset(c,false,sizeof(c));
memset(dis,INF,sizeof(dis));
dis[s]=0;
for(int t=1;t<=n;++t){
int id=-1;
ll mx=1e18;
for(int j=0;j<n;++j){
if(!c[j]&&mx>dis[j])
mx=dis[j],id=j;
}
memset(w,INF,sizeof(w)); // w 表示走一条原图边 + 若干条假边的最短路
// 这里必须赋值为 INF 而不是 w[x]=dis[x],因为要先走一条原图边
c[id]=true;
for(int j=head[id];~j;j=edge[j].next){
int to=(dis[id]+edge[j].to)%n; // 计算当前的终点
w[to]=std::min(w[to],dis[id]+edge[j].v); // 计算走一条原图边之后的最短路
}
for(int i=0;i<2*n;++i){ // 走假边(i<2*n 的原因是一个节点最多可能会走 n-1 条假边,最坏情况下 n-1 需要连续走到 0,1,2,...,n-2)
w[(i+1)%n]=std::min(w[(i+1)%n],w[i%n]+1);
}
for(int i=0;i<n;++i) // 更新 dis
dis[i]=std::min(dis[i],w[i]);
}
return;
}
int main(void){
memset(head,-1,sizeof(head));
n=read(),m=read();
for(int i=1;i<=m;++i){
int u=read(),v=read(),w=read();
add(u,v,w);
}
for(int i=0;i<n;++i){
Dijkstra(i);
for(int j=0;j<n;++j){
printf("%lld ",dis[j]);
}
puts("");
}
return 0;
}