刷题链接:
https://www.dotcpp.com/oj/train/1038/
题目一:C题 卡牌
看网上更多的是使用的二分,我是自己模拟的,先用大致的模拟思路写,然后再考虑能不能优化
1.对序列进行从小到大排序后,每次抽出一套牌前几个数总会减小,直到减小为0,然后再对前几个牌数为0的牌进行增加,前几个个数为0的牌肯定是同时增加的
2.不进行优化,每次对序列的前几个已经为0的数加一,每次对序列所有数减一,会导致超时
3.优化(贪心):就是想办法每次不加一减一,每次减尽可能最多(减去最小的即排序后的第一个),每次加尽可能最多(满足三个条件:1.bi>0 2.m足够前几个均分 3.最多加到第一个不为0的个数 )
AC代码:
#include<stdio.h>
#include<algorithm>
#include<iostream>
using namespace std;
typedef long long LL;
int n;
LL m; //注意这个数据范围
const int N=200005;
typedef pair<int,int>PII; //第一维存ai,第二维存下标,便于通过这个下标找对应的bi,就不需要自己写排序函数了
PII a[N];
int b[N];
int main()
{
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)
{
int data;
scanf("%d",&data);
a[i].first=data;
a[i].second=i;
}
for(int i=1;i<=n;i++) scanf("%d",&b[i]);
sort(a+1,a+n+1); //pair排序默认是按照first进行升序排列的
LL ans=0;
while(true) //m最后不一定为0,可能会有剩余,不适合用于循环结束判断
{
int end=0;
int t=a[1].first; //每次减少尽可能的多,减去数列中最小的
for(int i=1;i<=n;i++)
{
a[i].first-=t;
if(a[i].first==0) end=i;
}
ans+=t;
int minb=b[a[1].second]; //每次加回去尽可能多,前几个为0的同时加,满足三个条件:1.bi>0 2.m足够前几个均分 3.最多加到第一个不为0的数的数据
for(int i=1;i<=end;i++)
{
if(b[a[i].second]<minb) minb=b[a[i].second];
}
if(minb==0) break;
int j=minb;
for(;j>=1;j--)
{
if(m>=end*j) break;
}
if(j==0) break; //有边界情况就要考虑特殊判断一下,所有的ai都一样时
if(end<n) j=j<a[end+1].first?j:a[end+1].first;
for(int i=1;i<=end;i++)
{
if(b[a[i].second]==0)
{
m=0;
break;
}
a[i].first+=j;
b[a[i].second]-=j;
m-=j;
}
}
printf("%lld",ans);
return 0;
}
题目二:D题 最大数字
1.简单的dp问题,因为序列满足局部最优解,前i个数字构成的数最大其实是前i-1个数字构成的最大数加上最大化的第i个数字,前i个数字构成最大数的状态可以从前i-1个数构成最大数转移过来,但是要保存一下对前i-1个数字进行操作用了多少加和减操作
2.dp[i][j][k] 前i个数字最多进行j次加和最多进行k次减得到的最大数,然后考虑最后一步,第i个数字会进行多少次加和减操作,进行一下枚举
#include<iostream>
using namespace std;
typedef long long LL;
int n,A,B;
int num[20];
LL dp[20][105][105];
int Now(int i,int s,int x) //对第i位数字进行s次加和x次减后得到的数字
{
int tmp=num[i]+s-x;
if(tmp<0)
{
tmp=-tmp;
return (10-tmp%10)%10;
}
else return tmp%10;
}
LL Add(LL pre,LL now) //将第i个数字拼到后面
{
return pre*10+now;
}
int main()
{
char h=cin.get();
int ind=1;
while(h!=' ')
{
num[ind++]=h-'0';
h=cin.get();
}
cin>>A>>B;
dp[0][0][0]=0;
dp[1][0][0]=num[1];
int n=ind-1;
for(int i=1;i<=n;i++)
{
for(int j=0;j<=A;j++)
{
for(int k=0;k<=B;k++)
{
for(int s=0;s<=j;s++)
{
for(int x=0;x<=k;x++)
{
dp[i][j][k]=max(Add(dp[i-1][j-s][k-x],Now(i,s,x)),dp[i][j][k]);
}
}
}
}
}
cout<<dp[n][A][B]<<endl;
return 0;
}
题目三:E题 出差
简单的单源最短路径,隔离天数和路径天数是同等级别的,直接将到达某个目的地的隔离天数加到路径权值上
int类型的无穷大设置为0x3f3f3f3f (4个)
longlong类型的无穷大设置为0x3f3f3f3f3f3f3f3f(8个)
#include<iostream>
using namespace std;
const int N=1010;
const int INFI=0x3f3f3f3f; //这个记住了,一定要设置为最大的,1e8都不行 ,
int n,m;
int graph[N][N];
int geli[N];
int dist[N];
int visited[N];
int Findmin()
{
int min_i=-1,min=INFI;
for(int i=1;i<=n;i++)
{
if(!visited[i]&&dist[i]<min)
{
min=dist[i];
min_i=i;
}
}
return min_i;
}
void Dijistra(int s)
{
for(int i=1;i<=n;i++)
{
dist[i]=graph[s][i];
}
visited[s]=1;
while(true)
{
int min_i=Findmin();
if(min_i==-1) break;
visited[min_i]=1;
for(int i=1;i<=n;i++)
{
if(!visited[i]&&graph[min_i][i]!=INFI&&dist[min_i]+graph[min_i][i]<dist[i])
{
dist[i]=dist[min_i]+graph[min_i][i];
}
}
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>geli[i];
geli[1]=0;
geli[n]=0;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(i==j) graph[i][j]=0;
else graph[i][j]=INFI;
}
}
for(int i=0;i<m;i++)
{
int v1,v2,w;cin>>v1>>v2>>w;
if(v1==v2) //要注意考虑自环的情况,这道题多重边的好像没有涉及
{
graph[v1][v2]=0;
graph[v2][v1]=0;
}
else
{
graph[v1][v2]=w+geli[v2];
graph[v2][v1]=w+geli[v1];
}
}
Dijistra(1);
cout<<dist[n];
return 0;
}
题目四:F题 费用报销
简单的0/1背包变题
dp[i]表示前i张票据能凑成的最大费用
题目说的面值尽可能接近M其实就是面值最大
判断最后一个票据能不能要不要,如果不能要:dp[i][j]=dp[i-1][j];
如果能要(必须满足第i张票据的面值小于实际费用),再考虑要了和更大还是不要和更大
并且前一个票据必须与当前票据时间相差大于等于k天 dp[i][j]=max(dp[i][j],dp[h][j-w[num[i]]]+w[num[i]]);
错误思想:求与当前票据相差大于等于k天的所有dp[h][j-w[num[i]]]的最大值,再加上w[num[i]]
只需要从前一个状态转移过来,即dp[j],j就是前一个状态,j满足与与当前票据时间相差大于等于k天
dp[i]不是看最后一个选的哪一个,而是前个(与最长递增子序列的问题区别开)
AC代码:
#include<iostream>
#include<algorithm>
using namespace std;
int n,m,k;
const int N=1005;
int num[N];
int dp[N][5005];
int w[N];
int month[13]={0,31,28,31,30,31,30,31,31,30,31,30,31};
int main()
{
cin>>n>>m>>k;
for(int i=1;i<=n;i++)
{
int y,d,wi;cin>>y>>d>>wi;
for(int j=1;j<=y-1;j++)
num[i]+=month[j]; //将日期转换为距离1月1日的天数
num[i]+=d;
w[num[i]]=max(w[num[i]],wi); //按照日期存储当天的面值,如果有当天的重复票据,选最大的那个
}
sort(num+1,num+n+1); //将日期从小到大排序
for(int i=1;i<=n;i++)
{
int h=i-1;
while(h>0&&num[i]-num[h]<k) h--; //求与当前票据相差大于等于k天的最后一个状态
for(int j=1;j<=m;j++)
{
dp[i][j]=dp[i-1][j];
if(w[num[i]]<=j) //第i天的费用没有超过实际费用,可以选择是否要
{
dp[i][j]=max(dp[i][j],dp[h][j-w[num[i]]]+w[num[i]]);
}
}
}
cout<<dp[n][m];
return 0;
}
题目五:G题 故障
这是一道概率题,首先要读懂题目,用概率公式来做
题目读懂后剩下的就是简单的模拟了
P(Ni):故障原因i发生的概率
P(Mj/Ni) :故障原因i导致故障现象j的概率
P(Mj) :故障现象j的概率
P(MjNi):出现故障现象i并且原因是j的概率
P(MjNi) = P(Ni) * P(Mj/Ni)
分析样例:
首先求P(Ni),由于故障原因3不可能导致故障现象3的发生,因此故障原因3不考虑了,
注意一点: 只要故障现象中存在一个现象是该故障原因不可能产生的,就都不考虑该故障原因了
P(N1)=30/(30+20) = 0.6 P(N2)=20/(30+20)=0.4
再求每个故障原因导致题目要求的所有故障现象发生的概率P(Mj/Ni):
(注意这个j是可以取多个值的,表示这多个故障现象同时发生,此时由于各个故障现象是独立分布的,直接概率相乘即可)
P(M3/N1)= (现象1不发生的概率 * 现象2不发生的概率 * 现象3发生的概率 * 现象4不发生的概率 * 现象5不发生的概率),即1 * (1-50/100) * 33/100 * (1-25/100) * 1 = 0.12375
P(M3/N2)=(现象1不发生的概率* 现象2不发生的概率* 现象3发生的概率* 现象4不发生的概率* 现象5不发生的概率)=(1-30/100)* 1* 35/100 *1 *1= 0.245
最后
P(M3 N1) =0.6 *0.12375=0.07425
P(M3 N2)=0.4 * 0.245=0.098
这两个概率相加要为1,进行归一化:
P(M3 N1) =0.07425/(0.07425+0.098)= 0.4310595
P(M3 N2)=0.098/(0.07425+0.098)=0.56894
P(M3 N3)=0
AC代码:
#include<iostream>
#include<algorithm>
#include<iomanip>
#include<vector>
using namespace std;
int N,M,K;
const int CN=105;
double y[CN];
double yx[CN][CN];
int x[CN];
typedef pair<int,double>PID;
bool cmp(PID a,PID b)
{
if(a.second>b.second) return true;
else if(a.second==b.second)
{
if(a.first<b.first) return true;
}
return false;
}
vector<PID>ans; //输出结果,排序后输出
int main()
{
cin>>N>>M;
for(int i=1;i<=N;i++) cin>>y[i];
for(int i=1;i<=N;i++)
{
for(int j=1;j<=M;j++) cin>>yx[i][j];
}
cin>>K;
for(int i=1;i<=K;i++)
{
int t;cin>>t;x[t]=1; //做标记不存储
}
double sum=0;
for(int i=1;i<=N;i++)
{
bool have=true;
for(int j=1;j<=M;j++)
{
if(x[j]&&yx[i][j]==0)
{
have=false;break;
}
}
if(!have) y[i]=0;
sum+=y[i];
}
//求P(Ni),归一化
for(int i=1;i<=N;i++)
{
if(sum!=0) y[i]/=sum;
}
sum=0;
//求P(Mj/Ni)
for(int i=1;i<=N;i++)
{
if(!y[i]) continue;
double tmp=1;
for(int j=1;j<=M;j++)
{
if(yx[i][j]==0) tmp*=1;
//有这个现象
else if(x[j]) tmp*=yx[i][j]/100;
else tmp*=(1-yx[i][j]/100) ;
}
y[i]=tmp*y[i];
sum+=y[i];
}
for(int i=1;i<=N;i++)
{
if(sum!=0) y[i]/=sum;
ans.push_back({i,y[i]});
}
sort(ans.begin(),ans.end(),cmp);
for(int i=0;i<ans.size();i++)
{
cout<<ans[i].first<<" "<<fixed<<setprecision(2)<<ans[i].second*100<<endl;
}
return 0;
}
题目六:H题 机房
- 用简单的求最短路径的方法,比如dijstra、floyd、SPFA等由于是多次查询,属于是多源最短路径,数据达到100000,最快的SPFA求多源最短路径的时间复杂度是O(KE)*V,都会超时,因此这些方法都不行。
- 因此采用先求最近公共祖先,然后利用公式来求两个结点之间的最短距离的方法来求,求最近公共祖先有多种方法,包括向上标记法,倍增法和tarjan离线算法,这道题也就属于是模板题了
向上标记法最坏的情况下,时间复杂度为O(n*m),会超时
倍增法,时间复杂度为O(nlogn+mlogn),不会超时
tarjan离线算法,时间复杂度为O(n+m),不会超时
实际上这三个算法是求树里面的任意两个结点的最近公共祖先,进而根据公式求出两个结点间的最短路径。但图是特殊的树,将图中的任意一个结点作为根节点,从而将图转化为树,遍历与当前结点相连接的子节点时将自定义的父节点排除在外就行。后两者算法差不多,对于这道题的时间耗费都差不多。
求LCA的详细讲解,见博客:https://blog.csdn.net/m0_58642116/article/details/128550161?spm=1001.2014.3001.5501 - 数据达到100000,最好是用scanf和printf进行输入输出,减小时间损耗
倍增法AC代码:
#include<iostream>
#include<vector>
#include<queue>
#include<string.h>
#include<stdio.h>
using namespace std;
const int N=100005,INF=0x3f3f3f3f;
int n,m,x,y;
typedef pair<int,int>PII;
vector<int>tr[N]; //邻接表
queue<int>que;
int depth[N],dist[N],degree[N];
int fa[N][16];
void bfs(int s)
{
dist[s]=degree[s];
memset(depth,INF,sizeof(depth));
depth[0]=0;depth[s]=1;
que.push(s);
while(!que.empty())
{
int u=que.front();
que.pop();
for(int i=0;i<tr[u].size();i++)
{
int j=tr[u][i];
if(depth[j]==INF)
{
depth[j]=depth[u]+1;
dist[j]=dist[u]+degree[j];
que.push(j);
fa[j][0]=u;
for(int k=1;k<=15;k++)
{
fa[j][k]=fa[fa[j][k-1]][k-1];
}
}
}
}
}
int lca(int x,int y)
{
if(depth[x]<depth[y]) swap(x,y);
for(int k=15;k>=0;k--)
{
if(depth[fa[x][k]]>=depth[y])
{
x=fa[x][k];
}
}
if(x==y) return x;
for(int k=15;k>=0;k--)
{
if(fa[x][k]!=fa[y][k])
{
x=fa[x][k];
y=fa[y][k];
}
}
return fa[x][0];
}
int main()
{
scanf("%d %d",&n,&m);
for(int i=1;i<n;i++)
{
scanf("%d %d",&x,&y);
tr[x].push_back(y);
tr[y].push_back(x);
degree[x]++;
degree[y]++;
}
bfs(1);
for(int i=0;i<m;i++)
{
scanf("%d %d",&x,&y);
if(x==y) printf("%d\n",degree[x]);
else
{
int p=lca(x,y);
printf("%d\n",dist[x]+dist[y]-2*dist[p]+degree[p]);
}
}
return 0;
}
tarjan算法AC代码:
#include<iostream>
#include<vector>
#include<queue>
#include<string.h>
using namespace std;
const int N=100005,INF=0x3f3f3f3f;
int n,m,x,y;
typedef pair<int,int>PII;
vector<int>tr[N]; //邻接表
vector<PII>query[N]; //存储查询 ,first存与下标index相关查询的另一个结点值,second存询问编号用于存储结果
int p[N]; //并查集
int ans[N],dist[N],st[N],degree[N];
void dfs(int u,int fa)
{
for(int i=0;i<tr[u].size();i++)
{
int j=tr[u][i];
if(j==fa) continue; //遍历与u相连的除了u的父亲以外的结点
dist[j]=dist[u]+ degree[j];
dfs(j,u);
}
}
int find(int j)
{
if(p[j]!=j) p[j]=find(p[j]); //往上查找的同时合并并查集
return p[j];
}
void tarjan(int u)
{
st[u]=1; //正在遍历
for(int i=0;i<tr[u].size();i++)
{
int j=tr[u][i];
if(!st[j]) //没有遍历过
{
tarjan(j);
p[j]=u; //遍历完了一个子节点
}
}
//子树都遍历完,回溯到u结点,求与u相关的查询
for(int i=0;i<query[u].size();i++)
{
int j=query[u][i].first;
if(st[j]==2) //只有已经遍历完并回溯完的才能求值
{
int lca=find(j); //找到j所属并查集的祖先,进行合并O(1)
int id=query[u][i].second;
ans[id]=dist[u]+dist[j]-2*dist[lca]+degree[lca];
}
}
st[u]=2; //遍历并回溯完了
}
int main()
{
cin>>n>>m;
for(int i=1;i<n;i++)
{
cin>>x>>y;
tr[x].push_back(y);
tr[y].push_back(x);
degree[x]++;
degree[y]++;
}
for(int i=0;i<m;i++)
{
cin>>x>>y;
if(x==y) ans[i]=degree[x];
else
{
query[x].push_back({y,i});
query[y].push_back({x,i});
}
}
for(int i=1;i<=n;i++) p[i]=i;
dist[1]=degree[1];
dfs(1,-1); //随便令一个为根节点,求每个点到根节点的距离,根节点的父节点就令为-1
tarjan(1);
for(int i=0;i<m;i++)
{
cout<<ans[i]<<endl;
}
return 0;
}
题目七:I题 齿轮
- 根据相邻齿轮的线速度相同,可以推出第一个齿轮和最后一个齿轮的线速度也是相同的,则有
w1 * r1 = wn * rn ,则wn=w1*r1/rn =w1 * qi
因此只需要每次询问时遍历所有齿轮,查看是否存在两个齿轮使得商为qi,这样的时间复杂度会是O(Q * n *n)绝对超时; - 采用离线标记,先遍历所有的齿轮半径,将这些半径可能组成的商标记一下,这样在询问的时候直接看该qi是否被标记就可以了,离线标记时有两种方法:
第一种对于每个齿轮半径,判断该半径值的所有倍数是否为齿轮半径,如果是就将他俩的商标记(有些题解的思路)
标记代码:
但是分析代码这种标记方法是会超时的,考虑最坏情况,所有齿轮半径为1,那么a[i]就等于1,外层最大2e5,内层每次j+1,为2e5,最终耗时2e5 * 2e5,肯定会超时的。就算所有半径不是1而是2,最终耗时也是2e5*2e5/2,还是会超时,因此网上有些题解在dotcpp上过不去。//标记数组中任意两个数的商值 for(int i=1;i<=n;i++) { for(int j=a[i];j<=N;j+=a[i]) //N根据题目数据范围最大为200005; { if(st[j]) flag[j/a[i]]=1; } }
第二种对于每个齿轮半径,判断该半径值的所有约数是否为齿轮半径,如果是就将他俩的商标记
标记代码:
标记约束的最坏时间为2e5*sqrt(2e5)=1e7左右,不会超时//标记数组中任意两个数的商值,分解每个数的约数,将在数组中的约数标记为1就行 for(int i=1;i<=n;i++) { int up=sqrt(a[i]); for(int j=1;j<=up;j++) { if(a[i]%j==0) { //j在数组里,则商a[i]/j就标记为1 if(st[j]) flag[a[i]/j]=1; if(st[a[i]/j]) flag[j]=1; } } }
完整AC代码:
#include<iostream>
#include<stdio.h>
#include<math.h>
using namespace std;
const int N=200005;
int n,q,t;
int st[N];
int a[N],flag[N]; //flag存当前数组中的数能构成哪些商值,作为qi
int main()
{
scanf("%d %d",&n,&q);
for(int i=1;i<=n;i++)
{
scanf("%d",&t);
st[t]=1;
a[i]=t;
}
//标记数组中任意两个数的商值,分解每个数的约数,将在数组中的约数标记为1就行
for(int i=1;i<=n;i++)
{
int up=sqrt(a[i]);
for(int j=1;j<=up;j++)
{
if(a[i]%j==0)
{
//j在数组里,则商a[i]/j就标记为1
if(st[j]) flag[a[i]/j]=1;
if(st[a[i]/j]) flag[j]=1;
}
}
}
for(int i=0;i<q;i++)
{
scanf("%d",&t);
if(flag[t]) printf("YES\n");
else printf("NO\n");
}
return 0;
}
题目八:J题 搬砖
- 读题就能感觉出来是个0/1背包的问题,只不过约束不是背包的容量,而是上面i-1个砖块的重量不能超过第i个砖块的价值
- 于是定义dp[i][j] 就表示前i个砖块组成的塔重量为j的最大价值
假设前i个砖块组成的塔的重量为j,则前i-1个砖块组成的塔的重量就为j-wi,并且这前i-1个砖块组成的塔的重量j-wi还不能超过vi,实际就是加个判断就行 - 然后就是该按照什么顺序来枚举砖块?简单的0/1背包问题中每个物品之间是独立的,都只是受限于背包的容量,因此他们每个物品之间没有直接制约关系,但是这道题的砖块相互间有制约关系,因此要考虑选择砖块的顺序,实际上这就是要排序。这题我们可以这样强制定义让i在上面,j在下面,让wi+sum<vj(满足题意可采纳),wj+sum>vi(不满足题意不能采纳),也就是让i在上面的时候j可以选,让i在下面的时候j不可以选了。转化一下这个公式,可以把sum丢掉,sum是上面所有的重量。然后公式就是wi<vj,vi<wj。两个想加可以得到wi+vi<wj+vj。所以按这个排序就行。
- 这是一个经典的01背包+贪心的问题,贪心的排序处理原因也可以参考博客:
https://blog.csdn.net/m0_58642116/article/details/128647612?spm=1001.2014.3001.5501
AC代码:
#include<iostream>
#include<vector>
#include<algorithm>
#include<map>
using namespace std;
const int N=1005,M=20005;
struct Node
{
int w;
int v;
}a[N];
int n,m,w,v,dp[N][M],ans=0; //dp[i][j]表示前i个物品中选出来重量为j的最大价值
bool cmp(Node a,Node b)
{
return a.w+a.v<b.w+b.v; //注意是要加起来,而不是直接按照w排序
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i].w>>a[i].v; m+=a[i].w;
}
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
dp[i][j]=dp[i-1][j];
if(j>=a[i].w&&(j-a[i].w<=a[i].v)) //加个判断,限制一下前i-1个物品的重量就行了
{
dp[i][j]=max(dp[i][j],dp[i-1][j-a[i].w]+a[i].v);
}
}
}
for(int i=1;i<=m;i++) ans=max(ans,dp[n][i]);
cout<<ans;
return 0;
}
进行一下空间优化:
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1005,M=20005;
struct Node
{
int w;
int v;
}a[N];
int n,w,v,dp[M],ans=0; //dp[i][j]表示前i个物品中选出来重量为j的最大价值
bool cmp(Node a,Node b)
{
return a.w+a.v<b.w+b.v; //注意是要加起来,而不是直接按照w排序
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i].w>>a[i].v;
}
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;i++)
{
for(int j=a[i].w+a[i].v;j>=a[i].w;j--)
{
dp[j]=max(dp[j],dp[j-a[i].w]+a[i].v);
ans=max(ans,dp[j]);
}
}
cout<<ans;
return 0;
}