CF1350B
Solution
很简单的一道 题。
状态设计 表示从 号位置看到 号位置的选的数的数量的最大值。容易发现,下标为 的数可以跟在任何一个下标为 的数的后面,且必须满足其中 。
注意答案并不是 ,而是 数组中的最大值。注意将数 的约数的个数的时间复杂度约束在 以内。
时间复杂度为 ,即 。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
int t,n;
int a[100005],dp[100005];
signed main()
{
cin>>t;
while (t--)
{
cin>>n;
for (int i=1;i<=n;i++) cin>>a[i];
int ans=-1;
for (int i=1;i<=n;i++)
{
int k=i,maxv=0;
for (int j=1;j*j<=k;j++)
{
if (i%j==0)
{
if (a[j]<a[i]) maxv=max(maxv,dp[j]);
if (a[i/j]<a[i]) maxv=max(maxv,dp[i/j]);
}
}
dp[i]=maxv+1;
ans=max(ans,dp[i]);
}
cout<<ans<<endl;
}
return 0;
}
CF1349A
这不是一道水题,尽管这是某场比赛的 题。
Solution
容易发现,如果正整数 作为了 个数中至少 个数的约数,那么答案就一定有 这个因数。
因此,我们只需要枚举质数 加上一定的剪枝就可以了。即,如果已经看到了 个数并不是 的倍数,那么就直接跳出并宣布答案并不又含 这个因数;同时,如果发现它是 的倍数,就直接将它除以 ;这种除法一定不会影响答案的正确性,因为任何两个不同的质数一定互质。
注意答案可能含有多个质因子 ,所以我们要不停地用 来进行筛查,直到检查失败(即超过两个数无法被 整除)为止。答案就是所有 的积。
核心代码如下:
for (int i=1;i<=cnt;i++)
{
int count=0;
while (1)
{
int pos=0,flag=1;
for (int j=1;j<=n;j++)
{
if (a[j]%b[i]==0) a[j]/=b[i];
else pos++;
if (pos==2)
{
flag=0;
break;
}
}
if (flag==1) count++;
else break;
}
tot=tot*quick_power(b[i],count);
}
Code
#include <bits/stdc++.h>
#define int long long
#define inf 2000000007
using namespace std;
int n,tot=1,cnt=0;
int prime[200005],a[200005],b[200005];
int quick_power(int x,int y)
{
int res=1;
for (;y;y=y>>1,x=(x*x))
{
if (y&1) res=res*x;
}
return res;
}
signed main()
{
cin>>n;
for (int i=1;i<=n;i++) cin>>a[i];
for (int i=2;i<=200000;i++) prime[i]=1;
for (int i=2;i<=200000;i++)
{
if (prime[i]==0) continue;
for (int j=2*i;j<=200000;j+=i) prime[j]=0;
}
for (int i=2;i<=200000;i++)
{
if (prime[i]==1) b[++cnt]=i;
}
for (int i=1;i<=cnt;i++)
{
int count=0;
while (1)
{
int pos=0,flag=1;
for (int j=1;j<=n;j++)
{
if (a[j]%b[i]==0) a[j]/=b[i];
else pos++;
if (pos==2)
{
flag=0;
break;
}
}
if (flag==1) count++;
else break;
}
tot=tot*quick_power(b[i],count);
}
cout<<tot<<endl;
return 0;
}
P6512
蛮综合的一道好题!
Solution
Part 1
首先,我们把所有猪猪出现的时间从小到大排序。
考虑: 如果要抓第 只猪猪,那么第 只猪猪能否也被抓到。注意这里眼里只有第i只和第j只猪猪,没有别的猪猪。
假定第 只猪猪在 时刻, 号节点上出现;第 只猪猪在第 时刻, 号节点上出现 。如果从 到 的最短耗时不大于 与 的差(时间差),那么它们就可能一起被抓到;否则不可能一起被抓到(因为抓了一只就来不及赶过去抓另一只了呀)。
注意我们这里从 到 的最短耗时就是从 到 的最短路径;由于 不是很大,没必要用 Johnson全源最短路的板子,因为我们用简单的 就可以轻松跑过。然后两重循环枚举飞猪,再用上面的方法看看它们可不可能一起被抓到即可。
同时发现,如果第 只飞猪与第 只飞猪能够同时被抓到,就相当于从 到 连了一条单向边。
Part 2
仔细研究上一个Part的最后一句话。
恍然大悟的您发现: 我们就是要找到从 到 的最长路径,并且每个节点只能经过一次(一只可爱的猪猪只能被抓一次啊),答案就是上述路径的长度(每条边的长度均为 )。而且,因为总是从编号小的点连到编号大的点,所以说不可能有环。
下面就是一个显而易见的 了。拓扑排序没有必要,毕竟数据不大。
状态设计,即 表示从 到 号节点的最长路径( 为飞猪的数量)。
状态转移需要想一想。每一个节点都继承了它指向的点,那么我们就可以找到它所有指向的点中 值最大的一个,并让这个节点跟在它后面。
for (int i=head[now];i;i=e[i].next)
{
if (e[i].to>now) maxv=max(maxv,dp[e[i].to]);
}//找到最大值
dp[now]=maxv+1;//跟在它后面
Part 3
哎呀哎样例过不了了。怎么回事呢?
容易发现,从 号节点到 号猪猪(最早出现的猪猪)也需要时间的。
那么怎么办呢?很简单啊。
只需要加一个 号猪猪,它在 时刻, 号节点出现(即开始就能抓到它)。然后其他的正常操作就可以啦,记得带上可怜的 号猪猪哦。
而由于多了一只猪猪并且它肯定能够被抓到,所以答案还要减一。综上所述,答案为 。
Part 4
①时间复杂度:
②做题时间:
③综合难度: 绿
④个人思路标签: 图论+最短路+Floyd+快速排序+动规dp(递推)。
注意不要开long long,搞不好这样可能会导致MLE甚至TLE。
Code
#include <bits/stdc++.h>
#define inf 5000005
using namespace std;
int n,m,k,cnt=0;
int GA[205][205],head[5005],dp[5005];
struct edge
{
int next;
int to;
}e[30000000];
struct node
{
int t;
int rt;
}pig[5005];
bool cmp(node a,node b)
{
return a.t<b.t;
}
inline void add_edge(int u,int v)
{
cnt++;
e[cnt].to=v;
e[cnt].next=head[u];
head[u]=cnt;
}
signed main()
{
cin>>n>>m>>k;
for (int i=1;i<=n;i++)
{
for (int j=1;j<=n;j++)
{
if (i!=j) GA[i][j]=inf;
else GA[i][j]=0;
}
}
for (int i=1;i<=m;i++)
{
int u,v,w;
cin>>u>>v>>w;
GA[u][v]=w;
GA[v][u]=w;
}
for (int k=1;k<=n;k++)
{
for (int i=1;i<=n;i++)
{
for (int j=1;j<=n;j++)
{
if (GA[i][k]+GA[k][j]<GA[i][j])
{
GA[i][j]=GA[i][k]+GA[k][j];
GA[j][i]=GA[i][k]+GA[k][j];
}
}
}
}
for (int i=1;i<=k;i++) cin>>pig[i].t>>pig[i].rt;
sort(pig+1,pig+k+1,cmp);
pig[0].t=0,pig[0].rt=1;
for (int i=0;i<=k;i++)
{
for (int j=i+1;j<=k;j++)
{
if (GA[pig[i].rt][pig[j].rt]<=pig[j].t-pig[i].t) add_edge(i,j);
}
}
for (int now=k;now>=0;now--)
{
int maxv=0;
for (int i=head[now];i;i=e[i].next)
{
if (e[i].to>now) maxv=max(maxv,dp[e[i].to]);
}
dp[now]=maxv+1;
}
cout<<dp[0]-1<<endl;
return 0;
}
特别注意
下面几题与可爱的树树有关,所以需要两个前置芝士:
①链式前向星存图与遍历方法;
②深搜优先搜索 。
保证,下面的所有内容保证不会涉及到如考倍增LCA,树链剖分等提高组内容;但是会涉及到许多普及组内容,如树上深搜,宽搜,以及有关树的数学问题或者树上贪心等内容。
所以,大家都是能听得懂的啦~
CF1139C
Solution
如果直接计算答案,明显会很烦,想不清楚。
所以,我们决定从反面计算,并用所有的答案减去反面答案就是正面答案(宏观)。
容易发现,对于一个所有边都不是黑色的子树中,任选 个数作为 数组的 个数一定是反面情况,因为从 到 的路径上一定没有经过黑色的边。
注意所有考虑的无黑边子树均为独立的;换句话说,所有上述的子树均没有公共节点或公共边。根据乘法原理可得,假设一个无黑边子树的节点数为 ,那么在其中选 个节点作为 数组有且仅有 种情况。
根据加法原理,得到反面答案就是 。由于共有 个节点,那么所有答案的数量就是 。
综上所述,答案就是 。
一遍 即可跑出所有无黑边子树的大小,带着 时间的计算,所以时间复杂度就是 。
Pay Attention!
①带减法的取模十分特殊,不能直接减并取模,否则答案容易变成负数。
((a-b)%mod+mod)%mod
②注意单个节点不含有边,也就意味着单个节点也是一个无黑边的子树。
③为了防止 过于繁杂难写,本蒟蒻建议大家对于每一个节点都分别跑一遍 ,当然对于之前跑过的点看都不看,这样不仅使代码简单还没有增加时间复杂度。
但是,考虑到大家对树上题目的刷题量较少(我也同样),本蒟蒻详解一下本题的 :
inline void dfs2(int now,int fath)
//从now往下深搜找,统计含now的无黑边子树的大小
{
tot++;
visited[now]=1;//标记来过了
for (int i=head[now];i;i=e[i].next)
{
if (e[i].to!=fath&&e[i].xv==0) dfs2(e[i].to,now);
//注意我们只找无黑边,如果e[i].xv=1(红)那么我们就不深搜它孩子,否则会出现红边
}//dfs经典操作,if (e[i].to!=fath)的意义是找它的孩子
}
inline void dfs1(int now,int fath)//使用先序遍历找每个节点的全黑子树大小
{
tot=0;//在做dfs2前把tot清0
if (!visited[now]) dfs2(now,fath);//如果now号节点没来过,那么就深搜
ans=((ans-quick_power(tot,k))%mod+mod)%mod;//总的情况减去目前看到的不合法(反面)方案数,注意减法+取模的方式
for (int i=head[now];i;i=e[i].next)
{
if (e[i].to!=fath) dfs1(e[i].to,now);//继续深搜它的孩子
}
}
......
ans=quick_power(n,k);//总共的方案数
dfs1(1,0);//深搜,求出ans(答案)的值
不懂请看注释。
最后献上 代码~
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int mod=1e9+7;
int n,k,cnt=0,ans=0,tot=0;
int head[200005],visited[200005];
struct edge
{
int next;
int to;
bool xv;
}e[200005];
inline void add_edge(int u,int v,int w)
{
cnt++;
e[cnt].to=v;
e[cnt].xv=w;
e[cnt].next=head[u];
head[u]=cnt;
}
inline void dfs2(int now,int fath)
{
tot++;
visited[now]=1;
for (int i=head[now];i;i=e[i].next)
{
if (e[i].to!=fath&&e[i].xv==0) dfs2(e[i].to,now);
}
}
int quick_power(int a,int b)
{
int res=1;
for (;b;b=b>>1,a=(a*a)%mod)
{
if (b&1) res=(res*a)%mod;
}
return res;
}
inline void dfs1(int now,int fath)
{
tot=0;
if (!visited[now]) dfs2(now,fath);
ans=((ans-quick_power(tot,k))%mod+mod)%mod;
for (int i=head[now];i;i=e[i].next)
{
if (e[i].to!=fath) dfs1(e[i].to,now);
}
}
signed main()
{
cin>>n>>k;
for (int i=1;i<n;i++)
{
int u,v,w;
cin>>u>>v>>w;
add_edge(u,v,w);
add_edge(v,u,w);
}
ans=quick_power(n,k);
dfs1(1,0);
cout<<ans<<endl;
return 0;
}
CF1098A
Solution
简单的贪心。
我们需要在让答案存在的情况下,尽可能让深度小的节点的点权大,这样才能"造福"它的后代。原因显然,深度小的节点能够影响更多的节点(它的孩子)。如果它的点权变大那么它的许多子节点的点权就可以变小,也就可以答案更优。
但是,不能让深度小的节点的点权太大,否则它孩子的点权就变成负的了,这是绝对不可以的。换句话说,我们对于一个非叶节点的点权应该设定为它所有孩子的 的最小值。
如果最终构造出来的某个节点的点权为负,那么就应该输出-1;注意到这已经尽可能让所有点权非负了,如果还不行就说明无解。
上AC代码~
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n,ans=0,cnt=0;
int head[100005],s[100005],a[100005],minv[100005],ns[100005];
struct edge
{
int next;
int to;
}e[200005];
inline void add_edge(int u,int v)
{
cnt++;
e[cnt].to=v;
e[cnt].next=head[u];
head[u]=cnt;
}
inline void dfs(int now,int fath)
{
for (int i=head[now];i;i=e[i].next)
{
if (e[i].to!=fath) dfs(e[i].to,now);
}
for (int i=head[now];i;i=e[i].next)
{
if (e[i].to!=fath) minv[now]=min(minv[now],minv[e[i].to]);
}
minv[now]=min(minv[now],s[now]);
}
inline void dfs2(int now,int fath)
{
if (s[now]!=1000000007)
{
a[now]=s[now]-ns[fath];
ns[now]=ns[fath]+a[now];
}
else
{
int num;
if (minv[now]==1e9+7) num=0;
else num=minv[now]-ns[fath];
a[now]=num;
ns[now]=ns[fath]+num;
}
for (int i=head[now];i;i=e[i].next)
{
if (e[i].to!=fath) dfs2(e[i].to,now);
}
}
signed main()
{
cin>>n;
for (int i=2;i<=n;i++)
{
int u;
cin>>u;
add_edge(u,i);
add_edge(i,u);
}
for (int i=1;i<=n;i++)
{
cin>>s[i];
if (s[i]==-1) s[i]=1e9+7;
}
for (int i=1;i<=n;i++) minv[i]=1e9+8;
dfs(1,0);
dfs2(1,0);
for (int i=1;i<=n;i++)
{
if (a[i]<0) return cout<<-1<<endl,0;
}
for (int i=1;i<=n;i++) ans+=a[i];
cout<<ans<<endl;
return 0;
}
为了做下面的几个练习题,补上两个小芝士~
树的直径
树的直径,即树中最长的链。换句话说,就是树中任意两点之间的距离的最大值。
这里推崇两种办法:
① 法。
条件: 无负权路,给定的图为树。
首先,从 号节点开始,向下进行 ;然后找到离 号节点最远的那个节点 。注意 号节点一定为叶子节点,因为每条路都可能为直径做出贡献。
然后,换 号节点为根,再深搜一遍,找到离 号节点距离最远的 好节点,从 号节点到 号节点之间的路径就是树的最长链(直径), 和 就是该直径的两端。
② 法。
条件: 给定的图为树。
状态设计: 表示以 为根的子树中,以 为直径一端的最长链的长度。
首先,从 号节点开始进行深搜。假设目前 号节点的孩子已经深搜过了(有了 值),那么我们就可以推出 的值,并更新最长链的长度。
首先,显然 ,其中 表示 的孩子节点, 表示两点之间的最短路径。
同时,我们也要求出,在以 为根的子树中,经过 的最长链,并用这个值来更新直径的长度。显然, 。即,描述了到 的最长链,再经过 这条边,然后再继续选择另外一个孩子向下走的这个过程。
本蒟蒻将会白版演示一下:
for (int i=head[now];i;i=e[i].next)
{
if (e[i].to!=fath)
{
find_diameter(e[i].to,now);
len2=max(len2,dp[now]+dp[e[i].to]+e[i].dis);
dp[now]=max(dp[now],dp[e[i].to]+e[i].dis);
}
}
遍历两点之间的路径
假设我们要遍历的是从 到 的路径。
那么,我们可以暂时设 为根,然后先深搜一遍, 记录下以 为根的子树中是否含有 这个节点。如果含有则记 ,否则 。
然后,从 号节点开始向下搜,每次向着唯一一个使得 的孩子 ,直到到达了 号节点为止。
inline void dfs3(int now,int fath)
{
int flag=0;
for (int i=head[now];i;i=e[i].next)
{
if (e[i].to!=fath)
{
dfs3(e[i].to,now);
if (with[e[i].to]==1) flag=1;
}
}
if (flag==1||now==r) with[now]=1;
}
记录下以 为根的子树中是否含有 这个节点。如果含有则记 ,否则 。
inline void dfs4(int now,int fath)
{
for (int i=head[now];i;i=e[i].next)
{
if (e[i].to!=fath)
{
if (with[e[i].to]==1) dfs4(e[i].to,now);
}
}
}
}
直接从 深搜下去,每次向着唯一一个使得 的孩子 ,直到到达了 号节点为止。
注意,这种方法并不简便,用 做更快;但是问题在于,用 或倍增 的时间复杂度可以被我卡到 ;而使用记录+深搜的方法,在极端情况下时间复杂度为 。
所以,不要害怕繁杂,毕竟时间复杂度十分重要。
CF219D
作为练习题。
ABC133E
作为练习题。
P1099
作为练习题。
CF969B
值得一说,顺便与大家一起了解一下期望。
期望是个什么东西?可以吃吗?
期望,是指所有方案的答案之和,除以方案的数量。
比如,在 中任意选一个正整数的期望为 ;因为所有方案的和为 ,且方案数为 ,所以期望就是 。
在期望题中,经常要这么搞:
①分别找到所有方案的和与方案的数量;
②求出每一个方案的答案乘上它出现的概率 之和。
由于经验严重不足,所以本蒟蒻只能说这两个QAQ
Solution
分别考虑每一个的被访问编号的期望,并使用 有顺序地求出每一个节点的上述的值。
设节点 的访问编号为 ,且 号节点为 的父节点。容易发现,如果 的一位兄弟 被先访问了,那么它就让 加上了 ,其中 表示 的子树大小。
同时要注意, 从 转移而来;同时这里运用经验②(每一个方案的答案乘上它出现的概率之和),列出状态转移公式:
其中 表示 节点的兄弟数, 表示 节点的地 个兄弟, 表示 在 号节点之前被访问的概率。之所以要加上1,是因为它不可能与它父亲同时被访问。
发现,如果直接计算这个式子,时间复杂度直接上天。于是,我们看一下,能不能预处理出一些值呢?
首先,第一遍 我们可以预处理出每个节点的 值。
同时,发现 的值都是 !为什么?因为某个节点的兄弟一定在 号节点前被访问,或在 号节点后被访问;因此它在 之前被访问的概率就是 ,即 。
综上所述,递推式就是:
那么怎么 转移呢?
显然, ,再带入原式:
这就是最终得到的递推式,可以 转移。
时间复杂度 。
花絮
由于语言组织能力有一定问题,本蒟蒻决定模仿白鲟大佬写的题解,加上自己的理解与自己的解释,使正解更容易被理解与接受。
这是否会成为第一个您完全搞懂且最终一遍 的紫题?
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n,cnt=0;
int head[200005],size[200005];
double ans[200005];
struct edge
{
int next;
int to;
}e[200005];
inline void add_edge(int u,int v)
{
cnt++;
e[cnt].to=v;
e[cnt].next=head[u];
head[u]=cnt;
}
inline void dfs(int now,int fath)
{
for (int i=head[now];i;i=e[i].next)
{
if (e[i].to!=fath) dfs(e[i].to,now),size[now]+=size[e[i].to];
}
}
inline void dfs2(int now,int fath)
{
for (int i=head[now];i;i=e[i].next)
{
if (e[i].to!=fath)
{
ans[e[i].to]=ans[now]+(0.50*(size[now]-size[e[i].to]-1))+1.00;
dfs2(e[i].to,now);
}
}
}
signed main()
{
cin>>n;
for (int i=2;i<=n;i++)
{
int v;
cin>>v;
add_edge(i,v);
add_edge(v,i);
}
for (int i=1;i<=n;i++) size[i]=1;
ans[1]=1.00;
dfs(1,0);
dfs2(1,0);
for (int i=1;i<=n;i++) cout<<ans[i]<<' ';
cout<<endl;
return 0;
}
P1297
Description
第
题有
个选项。
做对了每一题,但是在最终填答题卡的时候填挫了,即第
题的答案填到了第
题上。特别的,第
题的答案填到了第
题上 (我也是醉了)
求可怜的 的正确题数的期望。
Solution
容易得到,做对题数的期望就是每题做对的概率之和。
做对一题当且仅当本题的答案与上一题的答案相同。因此,第 题正确的概率为 。分母为选项的组合的数量,分子为第 空正确的答案组合的数量。
若 ,那么所以答案就是 。
提前构造出数组 即可。时间复杂度为 。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n,A,B,C;
double ans=0.00;
int a[10000005];
signed main()
{
cin>>n>>A>>B>>C>>a[1];
for (int i=2;i<=n;i++) a[i]=(a[i-1]*A+B)%100000001;
for (int i=1;i<=n;i++) a[i]=a[i]%C+1;//3 2 4
a[0]=a[n];
for (int i=1;i<=n;i++)
{
double first=min(a[i],a[i-1])*1.00,second=a[i]*a[i-1];
ans+=first/second;
}
cout<<fixed<<setprecision(3)<<ans<<endl;
return 0;
}
既然扯到期望了,那么我们就说说期望的几道例题吧。
P6154
Description
给定一个 个点, 条有向边的无环图,求出路径长度的期望。
Solution
本题有套路式做法,即求出所有路径的长度之和与路径的条数。为了求这两个量,我们使用 ,即状态设计 表示目前发现到 点的所有路径的长度之和, 表示目前发现到 点的路径的数量。
在大部分的图形dp或树形dp中,任何一个节点的 值均转移自源于与它相邻的节点。假设我们现在想要得到 与 的值,那么它们必须由 推到而来( 与 有一条无向边,其中 连向 )。所以得到状态转移:
①
②
①表示, 的值要加上被它连接的那个点( )的 值,因为 计算的那些路径均可以通过加一条边 连到 ,所以有 这一步。
②表示,
的值要加上一些东西。甚么东西?即,
计算的路径均可以通过加一条边
连到
,这样每条路径的长度都变长了1;因此不仅要加上
,还要加上
。
事实上,您可以叫它记忆化搜索。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int mod=998244353;
int n,m,cnt=0,sumv=0,num=0;
int head[1000005],totl[1000005],cntl[1000005];
struct edge
{
int next;
int to;
}e[2000005];
inline void add_edge(int u,int v)
{
cnt++;
e[cnt].to=v;
e[cnt].next=head[u];
head[u]=cnt;
}
inline void dfs(int now)
{
if (cntl[now]!=0) return;
cntl[now]=1;
for (int i=head[now];i;i=e[i].next)
{
dfs(e[i].to);
cntl[now]=(cntl[now]+cntl[e[i].to])%mod;
totl[now]=(totl[now]+totl[e[i].to]+cntl[e[i].to])%mod;
}
}
int quick_power(int a,int b)
{
if (b==0) return 1;
int res=1;
for (;b;b=b>>1,a=(a*a)%mod)
{
if (b&1) res=(res*a)%mod;
}
return res;
}
inline int divide(int a,int b)
{
return (a*quick_power(b,mod-2))%mod;
}
signed main()
{
cin>>n>>m;
for (int i=1;i<=m;i++)
{
int u,v;
cin>>u>>v;
add_edge(u,v);
}
for (int i=1;i<=n;i++)
{
if (cntl[i]==0) dfs(i);
}
for (int i=1;i<=n;i++) sumv=(sumv+totl[i])%mod;
for (int i=1;i<=n;i++) num=(num+cntl[i])%mod;
cout<<divide(sumv,num)<<endl;
return 0;
}