2022/7/15 CSP-S模拟题总结
A.烽火传递
题目背景
烽火台是重要的军事防御设施,一般建在交通要道或险要处。一旦有军情发生,则白天用浓烟,晚上用火光传递军情。
题目描述
在某两个城市之间有 \(n\) 座烽火台,每个烽火台发出信号都有一定的代价。
为了使情报准确传递,在连续 \(m\) 个烽火台中至少要有一个发出信号。
现在输入 \(n\) 和每个烽火台发出信号的代价 \(a_i\),请计算总共最少需多少的代价才能实现两城市之间准确传递情报。
输入格式
第一行是用空格隔开两个数 \(n\) 和 \(m\);
第二行使用空格隔开的 \(n\) 个整数,表示每个烽火台发出信号的代价。
输出格式
输出仅一个整数,表示最小代价。
Solution
- 其实就是一道单调队列优化 \(\mathtt{DP}\),挂分是因为弹出队尾的条件 \(f[q[r]]>=f[i]\) 写成了 \(a[q[r]>a[i]]\) (悲);
AC code
#include<bits/stdc++.h>
using namespace std;
inline int read(){
int s=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)){
s=s*10+int(ch-'0');
ch=getchar();
}
return s*f;
}
const int N=2e5+10;
int n,m;
int a[N];
int f[N],q[N];
int main(){
// freopen("signal.in","r",stdin);
// freopen("signal.out","w",stdout);
n=read(),m=read();
for(int i=1;i<=n;++i)
a[i]=read();
int l=1,r=1;
for(int i=1;i<=n;++i){
while(l<r && i-q[l]>m) ++l;
f[i]=f[q[l]]+a[i];
// if(i==1331) cerr<<q[l]<<endl;
while(l<=r && f[i]<=f[q[r]])
--r;
q[++r]=i;
// printf("%d %d\n",i,f[i]);
}
int ans=0x3f3f3f3f;
for(int i=max(1,n-m+1);i<=n;++i)
ans=min(ans,f[i]);
// cerr<<ans<<endl;
printf("%d",ans);
return 0;
}
/*
5 3
1 2 5 6 2
*/
B.小R与回家
题目背景
小R 在火星玩累了,打算回家, 这是一个时空旅行的故事。
题目描述
小R 要从火星(编号为 \(1\))回到地球(编号为 \(n\)),宇宙中一共有 \(n\) 个星球,星球之间有一些航线,但是有些星球间的航线可能导致时间倒流,并且星球之间的航线并不保证可逆(即 \(a\) 到 \(b\) 的时间和 \(b\) 到 \(a\) 的时间不一定相同)。
时空旅行要求不能在出发时间之前到达最终目的地,否则会导致时空错乱,危及生命。
小R 的飞船上有速度调节装置,可以用来改变星球间飞行的时间,即可以将整次时空旅行中两两星球之间的时间增加或减少同一个整数。
小R 很想家,你能帮助他求出最快回家的时间。
输入格式
输入文件包含多组数据,第 1 个数为 \(T\),表示数据组数。
对于每组数据,输入第 1 行为两个正整数 \(n,m\),为星球的个数和星球间的路线数。
接下来 \(m\) 行,每行三个整数 \(i,j\) 和 \(t\), 表示由星球 \(i\) 到星球 \(j\) 飞行的时间为 \(t\)。
由 \(i\) 到 \(j\) 最多只会有一条飞行线路。
输出格式
输出文件共 \(T\) 行,每组数据输出一行。
如果可以通过调节速度调节器完成任务,则输出一个非负整数,表示由星球 \(1\) 到星球 \(n\) 的最短时间(注意最短时间要大于或者等于 0)。
如果不能由火星回到地球,则输出 -1。
Solution
-
最短路(然而考场上没有完全看出来甚至试图写费用流……)
-
题意翻译一下就是在一张有向图上找一条权值和非负的最短路,可以给所有边增加或减少一个数 \(t\);
-
考虑二分答案。因为图中很可能含有负边权,所以考虑使用 \(\mathtt{spfa}\) 来统计最短路的答案。
对没错它又复活了 -
先用几遍 \(\mathtt{dfs}\) 处理掉所有与 \(s\) 或 \(t\) 不连通的点,然后开始对答案进行二分;
设置以下几个函数:- \(\mathtt{check(x)}\) 直接判断 \(x\) 是否可以作为边权的调整值,是则返回 1.其中复合 \(\mathtt{count(i,x)}\) 函数和 \(\mathtt{spfa(x)}\) 函数;
- \(\mathtt{count(i,x)}\) 函数,\(i\) 表示处于点 \(i\),\(x\) 表示边权调整值,主要用于扫描图中的负环,如果当前调整值情况下存在负环,返回 1,否则返回 0;
- \(\mathtt{spfa(x)}\) 用于计算在调整值为 \(x\) 的情况下的最短路;
-
判断思路为:在二分出一个 \(x\) 时,扔进 \(\mathtt{check()}\) 函数里,在函数里我们需要做的是:
- \(\mathtt{count()}\) 判断是否存在负环。从每个未被筛掉的点出发,扫掉所有调整后边权为正的边(相当于只跑负权边),如果有一个点在一次搜索中被访问了两次则说明存在负环,直接 \(\mathtt{return\ 1}\);
- 如果接收到 \(\mathtt{count()}\) 的返回值为 \(1\),就可以直接 \(\mathtt{return\ 0}\),因为存在负环说明没有最短路;
- 如果没有负环,则用 \(\mathtt{spfa()}\) 计算最短路。
- 如果最短路算出来 \(<0\),则不符合题意,\(\mathtt{return\ 0}\);
- 否则 \(\mathtt{return\ 1}\),并在二分中计算答案。
AC code
#include<bits/stdc++.h>
using namespace std;
inline int read(){
int s=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)){
s=s*10+int(ch-'0');
ch=getchar();
}
return s*f;
}
const int N=105;
const int M=1e5;
const int Inf=0x3f3f3f3f;
struct memr{
int ans,d;
}b[N];
int T;
int n,m,s,t,k;
int head[M],ver[M],nxt[M],edge[M],tot=0;
void add(int x,int y,int z){
ver[++tot]=y,edge[tot]=z;
nxt[tot]=head[x],head[x]=tot;
return ;
}
int dist[N],vis[N],e[N];
void spfa(int c){
memset(dist,Inf,sizeof(dist));
memset(vis,0,sizeof(vis));
dist[s]=0;
vis[s]=1;
queue<int>q;
q.push(s);
while(q.size()){
int x=q.front();
q.pop();
for(int i=head[x];i;i=nxt[i]){
if(!e[ver[i]]) continue;
if(dist[ver[i]]<=dist[x]+edge[i]+c) continue;
dist[ver[i]]=dist[x]+edge[i]+c;
if(!vis[ver[i]]){
q.push(ver[i]);
vis[ver[i]]=1;
}
}
vis[x]=0;
}
return ;
}
bool count(int x,int cnt){
vis[x]=1;
for(int i=head[x];i;i=nxt[i]){
if(!e[ver[i]]) continue;
if(dist[ver[i]]<=dist[x]+edge[i]+cnt) continue;
if(vis[ver[i]]) return 1;
dist[ver[i]]=dist[x]+edge[i]+cnt;
if(count(ver[i],cnt))
return 1;
}
vis[x]=0;
return 0;
}
bool check(int x){
for(int i=1;i<=n;++i){
if(!e[i]) continue;
memset(vis,0,sizeof(vis));
memset(dist,Inf,sizeof(dist));
if(count(i,x)) return 0;
}
spfa(x);
if(dist[n]>=0) return 1;
return 0;
}
void check_p(int x){
if(vis[x]) return ;
vis[x]=1;
for(int i=head[x];i;i=nxt[i])
check_p(ver[i]);
return ;
}
int main(){
T=read();
while(T--){
memset(head,0,sizeof(head));
n=read(),m=read();
int u,v,w;
s=1,t=n;
for(int i=1;i<=m;++i){
u=read(),v=read(),w=read();
add(u,v,w);
}
memset(e,1,sizeof(e));
memset(vis,0,sizeof(vis));
check_p(1);
for(int i=1;i<=n;++i)
if(!vis[i]) e[i]=0;
for(int i=1;i<=n;++i){
if(!e[i]) continue;
memset(vis,0,sizeof(vis));
check_p(i);
if(!vis[t]) e[i]=0;
}
int l=-M,r=M;
int ans=Inf;
while(l<r){
int mid=(l+r)>>1;
if(check(mid)){
ans=dist[t];
r=mid;
}
else l=mid+1;
}
if(ans>1e9)
puts("-1");
else printf("%d\n",ans);
}
return 0;
}
C.疯狂的火神
题目描述
火神为了检验 \(zone\) 的力量,他决定单挑 \(n\) 个人。
由于火神训练时间有限,最多只有 \(t\) 分钟,所以他可以选择一部分人来单挑,由于有丽子的帮助,他得到了每个人特定的价值,每个人的价值由一个三元组 \((a,b,c)\) 组成,表示如果火神在第 \(x\) 分钟单挑这个人,他就会得到 \(a-b\times x\) 的经验值,并且他需要 \(c\) 分钟来打倒这个人。
现在火神想知道,他最多可以得到多少经验值,由于火神本来就很笨,进入 \(zone\) 的疯狂的火神就更笨了,所以他希望你来帮他计算出他最多可以得到多少经验值。
输入格式
第一行一个正整数 \(T\),表示数据组数
对于每组数据,第一行为两个正整数 \(n\) 和 \(t\),表示跟火神单挑的人的个数和火神的训练时间。
下面 \(n\) 行,每行三个正整数 \(A_i\),\(B_i\),\(C_i\),表示每个人的价值,含义见题目。
输出格式
对于每组数据输出一行一个整数表示火神最多能得到多少经验值
Solution
-
类似于一道背包,但要根据最优策略按照 \(b_i/c_i>=b_{i+1}/c_{i+1}\) 先排一遍序;
-
状态设计 \(f_{i,j}\) 表示单挑到第 \(i\) 个人时(可能不单挑第 \(i\) 人)恰好用了 \(j\) 分钟的最大经验收益;
AC code
#include<bits/stdc++.h>
using namespace std;
inline int read(){
int s=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)){
s=s*10+int(ch-'0');
ch=getchar();
}
return s*f;
}
const int N=1e3+10;
#define ll long long
int T;
int n,t;
ll f[N][N<<2];
struct memr{
int a,b,c;
bool operator<(const memr &_){
return (double)b/c>(double)_.b/_.c;
}
}in[N];
int main(){
// freopen("crazy.in","r",stdin);
// freopen("crazy.out","w",stdout);
T=read();
while(T--){
n=read(),t=read();
for(int i=1;i<=n;++i){
in[i].a=read(),in[i].b=read(),in[i].c=read();
}
sort(in+1,in+n+1);
memset(f,0,sizeof(f));
for(int i=1;i<=n;++i){
for(int j=t;j;--j){
if(j>=in[i].c)
f[i][j]=max(f[i][j],f[i-1][j-in[i].c]+in[i].a-in[i].b*j);
f[i][j]=max(f[i][j],f[i-1][j]);
}
}
ll ans=0;
for(int i=1;i<=t;++i)
ans=max(ans,f[n][i]);
printf("%lld\n",ans);
}
return 0;
}
/*
1
4 10
110 5 9
30 2 1
80 4 8
50 3 2
*/
D.火神的鱼
题目背景
火神最爱的就是吃鱼了,所以某一天他来到了一个池塘边捕鱼。池塘可以看成一个二维的平面,而他的渔网可以看成一个与坐标轴平行的矩形。
题目描述
池塘里的鱼不停地在水中游动,可以看成一些点。有的时候会有鱼游进渔网,有的时候也会有鱼游出渔网。所以火神不知道什么时候收网才可以抓住最多的鱼,现在他寻求你的帮助。
他对池塘里的每条鱼都给予了一个标号,分别从 1 到 \(n\) 标号,\(n\)表示池塘里鱼的总数。鱼的游动可以概括为两个动作:
1 l r d:表示标号在 \([l,r]\) 这个区间内的鱼向 \(x\) 轴正方向游动了 \(d\) 个单位长度。
2 l r d:表示标号在 \([l,r]\) 这个区间内的鱼向 \(y\) 轴正方向游动了 \(d\) 个单位长度。
在某些时刻火神会询问你,现在有多少条他关心的鱼在渔网内(边界上的也算),请你来帮助他吧。
输入格式
第一行包含一个整数 \(T\),表示测试数据组数,对于每组测试数据:
第一行包含一个整数 \(n\) 表示鱼的总数。
第二行包含四个整数 \(x_1,y_1,x_2,y_2\),表示渔网的左下角坐标和右上角坐标。
接下来n行每行两个整数 \(x_i,y_i\),表示标号为i的鱼初始时刻的坐标。
接下来一行包含一个整数 \(m\),表示后面的事件数目。
接下来的 \(m\) 行,每行为以下三种类型的一种:
1 l r d:表示标号在 \([l,r]\) 这个区间内的鱼向 \(x\) 轴正方向游动了 \(d\) 个单位长度。
2 l r d:表示标号在 \([l,r]\) 这个区间内的鱼向 \(y\) 轴正方向游动了 \(d\) 个单位长度。
3 l r:表示询问现在标号在 \([l,r]\) 这个区间内的鱼有多少在渔网内。
输出格式
对于每组数据的每个询问,输出一个整数表示对应的答案。
Solution
-
火神你怎么那么多事; -
线段树,对 \(x\) 轴和 \(y\) 轴分别建一棵线段树;

浙公网安备 33010602011771号