I题:
简化题意:给出举例d和一条长度为l的线段,每走一段长度为1的距离都需要花费一定的体力,而你拥有3次直接跳过长度为d的线段的机会。问最少需要多少体力才能到线段终点
我在赛场上把这题建模为找三条长度为d的线段使他们的和最大(),然后就一直贪心贪寄了()。这警告我们不要乱用贪心
实际上如果对dp熟悉的话这题基本一眼dp。突然感觉自己的水平拉完了因为后面的状态完全可以由前面转移过来。因为存在俩种走路方式:跳跃和走一段1,且存在边界限制(<=l)。所以直接正向dp是可以解决的.定义dp数组一维为剩余的跳跃次数i,一维为当前位置j。
如果是走一段1:dp[i][j+1]=min( dp[i][j+1] , dp[i][j]+cost[j+1] ).
如果跳跃:dp[i+1][min(l,j+d)]=min(dp[i+1][min(l,j+d)] , dp[i][j]);代码如下:
查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define ull unsigned long long
const int N=2e5+5;
const int INF=1e18;
void solve(){
int n,d;cin>>n>>d;
int t0,t1,t2;
cin>>t0>>t1>>t2;
string s;cin>>s;
vector<int> a(n+1);
vector<vector<int>> dp(4,vector<int>(n+1,INF));
for(int i=0;i<n;i++){
if(s[i]=='0')a[i+1]=t0;
else if(s[i]=='1')a[i+1]=t1;
else a[i+1]=t2;
}
for(int i=0;i<4;i++)dp[i][0]=0;
for(int i=0;i<4;i++){
for(int j=0;j<=n;j++){
if(j<n)dp[i][j+1]=min(dp[i][j+1],dp[i][j]+a[j+1]);
if(i<3){
int mx=min(n,j+d);
dp[i+1][mx]=min(dp[i+1][mx],dp[i][j]);
}
}
}
cout<<dp[3][n];
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int _=1;
//cin>>_;
while(_--)solve();
}
/*
10 2
10 20 5
0000000000
12 3
10 100 1
111000222000
*/
H题:(默认你已经了解了Nim博弈,如果不了解我之前有专门的Nim博弈专题讲这题,链接)
简化题意:给出n个数和一个限制k,A和B只能在限制内选择一个数(<=k)将n个数其中一个减去该数,直到有人不得不选0时,此人失败。
查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
void solve(){
int n,k;cin>>n>>k;
int x=0;
for(int i=1;i<=n;i++){
int y;cin>>y;
x^=y;
}
if(x!=0 && (x&-x)<=k)cout<<"Alice";
else cout<<"Bob";
cout<<'\n';
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int _=1;
cin>>_;
while(_--)solve();
}
C题:(默认你已经会了矩阵快速幂,如果不会我之前有矩阵快速幂专题专门讲这题,链接)
首先我们将建边的时间作为边权,其次这个询问看起来很绕,我们可以拿样例举例:

比如询问6 5时因为到不了输出-1;询问2 3时,若w=1,则恰好一步可达,若w为3则2到3之后要先去1再回3,又1 3之间存在双向边且边权都为0,因此最大值仍为0.询问2 6时,如果w为1,则必不可达,因为没有直接过去的路径,如果w为3,则需要在5的位置自环走一圈再到6,此时最大值为5到6的边权4.
理解了题目之后,首先我们如果没见过类似的题目,肯定会把这题当纯图论,直接就顺手用邻接表或者链式前向星存图了。但是这题有一个点就是n的范围是要≤200的,这是一个显示提示,我们以后见到这类的显示提示可以从dp,dfs或者枚举等方面入手。本题的话是从邻接矩阵建图为切入点的,当我们用邻接矩阵记录下样例的第一个状态(初始化为正无穷):

这是只走一步时的状态矩阵,当我们走第二步时,我们可以列出:

观察俩步的状态矩阵,会发现原本从i到j只需要1步,而现在需要在中间多经过一个点k,即从i到k,再从k到j。因此第二个矩阵中对应的值就为min( A2(i,j), max( A1(i,k) , A1(k,j) ) ).进一步推广到更高次幂的状态,会发现:
A3可以由A1和A2推出,值可以为min( A3(i,j) ,max( A1(i,k) , A2(k,j) ))或min( A3(i,j) ,max( A2(i,k) , A1(k,j) ));A4的矩阵可以由A3和A1推出,也可以由俩个A2推出,出于后续处理的便利,我们规定A^k由A^(k-1)与A推出.(实际上换一种推出方式最后的结果是不会变的)
查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define ull unsigned long long
const int N=205;
const int INF=1e9;
int n,m,k,q,w;
struct pi{
int a[N][N];
pi(){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++)a[i][j]=INF;
}
}
};
pi operator*(pi x,pi y){
pi t;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
for(int k=1;k<=n;k++){
t.a[i][j]=min(t.a[i][j],max(x.a[i][k],y.a[k][j]));
}
}
}
return t;
}
pi pq(pi A,int k){
pi res=A;
k--;
while(k){
if(k&1)res=res*A;
A=A*A;
k>>=1;
}
return res;
}
void solve(){
cin>>n>>m;
pi A;
for(int i=1;i<=m;i++){
int u,v;cin>>u>>v;
A.a[u][v]=0;
}
cin>>k;
for(int i=1;i<=k;i++){
int u,v;cin>>u>>v;
A.a[u][v]=min(A.a[u][v],i);
}
cin>>q>>w;
pi ans=pq(A,w);
int st,ed;
for(int i=1;i<=q;i++){
cin>>st>>ed;
int x=ans.a[st][ed];
if(x!=INF)cout<<x<<'\n';
else cout<<"-1\n";
}
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int _=1;
//cin>>_;
while(_--)solve();
}
J题:(默认你已经会了Kruskal重构树)
简化题意:给出n个点和点权,每俩个点之间的边权为gcd(ai,aj),有q个询问,每个询问给出起点和终点,求所有路径中一条路径上边权最小值的最大值。
首先,在任意无向图中,两点之间所有路径的最小边权的最大值,等于该两点在最大生成树上唯一路径的最小边权。
这是因为Kruskal算法按边权从大到小添加边,首次将两点连通的那条边就是该瓶颈值。同样的,如果问题是求所有路径的最大边权的最小值,那么这个最小值一定是在该图的最小生成树上的。因此我们只需构造原图的最大生成树,然后回答树上路径最小值查询。
对于一个完全图,如果直接枚举边会直接爆掉(因为点的数量是可以达到1e6次的,边数是可以达到1e12条的),因此此时的问题就变成了如何减少需要建立的边数,准确来说是有效边。此时我们就得通过边的性质来思考了,首先边权是等于gcd(au,av)的,我们如果假设其值为d,那么au和av实际上都是可以被d整除的,所以我们可以用一种缩点的思想:将所有点权能被d整除的点缩为一个点(联通块)。这里我举个具体的例子:
假设有4个城市,点权分别为:
6,15,10,30
最大点权mx=30。初始每个城市自成一个连通块,根就是自己:1,2,3,4。首先从d=30开始,会发现只有30可以被整除,因此只有一个根节点,不用合并;之后d=15,会发现15可以被整除,此时15和30就是俩个根节点,创建新节点(点权为15)并合并这俩个节点;之后d=10,发现10和30都可以被整除,出现了俩个根节点,合并(新节点点权10);最后d=6发现可以被30整除,再创建新节点(点权6)合并。因为每颗树的根节点一定是整颗树中数最小的值,也代表了整颗子树的gcd上限,此时这棵Kruskal树就可以通过lca得到最终结果。所以本题的巧妙之处就在于gcd条件的转化,通过枚举d的缩点思想来实现建树。这里给出建树代码:
for(int d=mx;d>=1;d--){
vector<int> roots;
for(int k=d;k<=mx;k+=d){
for(int u:pos[k]){
int r=dsu.find(u);
if(vis[r]!=d){
vis[r]=d;
roots.push_back(r);
}
}
}
if(roots.size()>=2){
val[++idx]=d;
for(int r:roots){
e[idx].push_back(r);
dsu.fa[r]=idx;
}
dsu.fa[idx]=idx;
}
}
看到三个for循环我相信你一定会问:这时间复杂度能过?接下来我们来证明:
外层循环d从mx到1,最多1e6.内层枚举所有d的倍数,次数约为mx/d。因此枚举总次数为

这是调和级数的经典结果。对于每个倍数k,我们需要遍历pos[k]中的所有点。那么我们可以将其转化为一个点i会被访问多少次,实际上访问次数=i的因数个数。对于ai<=1e6,最多的约数数量大约240,平均100(网上会有一些结论和证明可以自行寻找),因此整体访问次数为n*100,最坏达到1e8次。
综上,时间复杂度为O(MlogM+nlogM)其中n,m都可为1e6,实际可以在3s时限内运行完成。
以下是完整代码:
查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define ull unsigned long long
const int N=1e6+5;
int n,q;
int a[N*2],val[N*2],vis[N*2];
vector<int> pos[N*2],dep,e[N*2];
vector<vector<int>> up;
struct DSU{
vector<int> fa;
DSU(int n){
fa.assign(n+1,0);
for(int i=1;i<=n;i++)fa[i]=i;
}
int find(int x){
return x==fa[x]?x:fa[x]=find(fa[x]);
}
void in(int a,int b){
int x=find(a),y=find(b);
if(x==y)return;
fa[x]=y;
}
};
void dfs(int u,int f){
dep[u]=dep[f]+1;
up[u][0]=f;
for(int i=1;i<=20;i++)up[u][i]=up[up[u][i-1]][i-1];
for(auto v:e[u])dfs(v,u);
}
int lca(int x,int y){
if(dep[x]<dep[y])swap(x,y);
for(int i=20;i>=0;i--)if(dep[up[x][i]]>=dep[y])x=up[x][i];
if(x==y)return x;
for(int i=20;i>=0;i--){
if(up[x][i]!=up[y][i]){
x=up[x][i];
y=up[y][i];
}
}
return up[x][0];
}
void solve(){
cin>>n>>q;
int mx=-1;
for(int i=1;i<=n;i++){
cin>>a[i];
mx=max(mx,a[i]);
pos[a[i]].push_back(i);
}
int idx=n;
DSU dsu(n+N);
for(int d=mx;d>=1;d--){
vector<int> roots;
for(int k=d;k<=mx;k+=d){
for(int u:pos[k]){
int r=dsu.find(u);
if(vis[r]!=d){
vis[r]=d;
roots.push_back(r);
}
}
}
if(roots.size()>=2){
val[++idx]=d;
for(int r:roots){
e[idx].push_back(r);
dsu.fa[r]=idx;
}
dsu.fa[idx]=idx;
}
}
dep.assign(idx+1,0);
up.assign(idx+1,vector<int>(21,0));
dfs(idx,0);
for(int i=1;i<=q;i++){
int x,y;cin>>x>>y;
if(x==y)cout<<a[x]<<'\n';
else cout<<val[lca(x,y)]<<'\n';
}
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int _=1;
//cin>>_;
while(_--)solve();
}
/*
6 3
6 15 22 33 35 63
5 6
3 5
2 2
*/
浙公网安备 33010602011771号