感受——
能有什么感受!?我真的不好骂自己的!
2019年相对来说还是比后面几年的题目都简单的,第三题的DP我之前都做过,然后到了这里再做居然一脸懵想不出来!我的图论简直是爆炸!!!最后一题完完全全就是个SPFA的模板题(当然别的也行),然后我SPFA不会写!规则自己推不出来!
解析——
T1 数字游戏(number)
T1.1 思路处理
第一题没什么好说的,最基础的字符串输入,循环,加个判断就完事了。当然,如果想追求更高效一点的,因为
s
t
r
i
n
g
string
string类只能用
c
i
n
cin
cin,所以不妨把同步流给关了,用
c
o
u
t
cout
cout的输出。(这好像是句废话)
直接附代码。
T1.2 代码
#include<bits/stdc++.h>
using namespace std;
int main(){
//freopen("number.in","r",stdin);
//freopen("number.out","w",stdout);
cin.tie(0);cout.tie(0);ios::sync_with_stdio(0);//关闭同步流
string s;
cin>>s;
int cnt=0;
for(int i=0;i<s.size();++i)
cnt+=s[i]=='1';//直接加bool值完事
cout<<cnt;
return 0;
}
T2 公交换乘(transfer)
T2.1 思路处理
我自己都想不到这一题会花费我那么多的时间,最后还没做出最优解,虽然过了。我的复杂度是
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)。
这题单看一眼题面要求还是挺简单:坐地铁给地铁票,钱是一定要花的;坐公交车可能能省钱,在满足以下几个条件的情况下用地铁券——
1、公交乘车时间-地铁乘车时间
<
=
45
<=45
<=45
2、公交价格
<
=
<=
<=地铁价格
3、如果有就找到最早的地铁票
4、没有还是得花钱(别想逃票!!!)
这是我对题面的分析,在这里直接写建立在各位自己读过题的情况下。
然后我就陷入了迷茫。
题面上说“搭乘公交车时,如果可以使用优惠票一定会使用优惠票;如果有多张优惠票满足条件,则优先消耗获得最早的优惠票。”,这是我们刚刚拎出来的第三点,看上去很简单,但是时间和价格的两重限制,让我们似乎没法用贪心之类的方式解决,也没有什么高效的优化方式,只能采用暴力查找所有地铁票的方式。
这一查可不要紧,瞅一眼时间范围——
对于100%的数据, n n n≤ 1 0 5 10^5 105, t i ti ti≤ 1 0 9 10^9 109, 1 1 1≤ p r i c e i pricei pricei≤ 1000 1000 1000
那么按照我们暴力查的算法,最坏的情况很容易得出,前面乘
n
/
2
n/2
n/2次地铁,后面
n
/
2
n/2
n/2次公交车,每次查一遍
O
(
n
/
2
)
O(n/2)
O(n/2),最后总时间复杂度就是
O
(
N
2
/
4
)
O(N^2/4)
O(N2/4)。
1
0
5
10^5
105的时候,
寄了……
我没有想到更好的办法,然后就先打了个暴力,贴暴力部分的代码给各位。
for(ll j=1;j<=len1;++j){//所有的地铁票
if(sub[j].ti>bus[i].ti)//小小优化一下
break;
if(sub[j].pi>=bus[i].pi&&bus[i].ti-sub[j].ti<=45&&!vis[j]){//满足两个条件,同时没用
vis[j]=1;
f=0;//这个是标记
break;//找到就停
}
}
然后我就开始了长考……(难道T2就要出节目效果吗!!)
说来非常有意思,我的这个思路几乎是危险的边缘疯狂试探,但是也有一定道理。还是看刚刚的那个数据范围,这里我只给两个部分。
“
n
n
n≤
1
0
5
10^5
105,
t
i
ti
ti≤
1
0
9
10^9
109” “一个正整数
n
n
n,代表乘车记录的数量”
我们先把
t
i
ti
ti和
n
i
ni
ni除一下
1
0
9
/
1
0
4
=
1
0
5
10^9/10^4=10^5
109/104=105
然后再看一下公交车用票的第一个条件"时间
<
=
45
<=45
<=45"
这意味着什么?!
这意味按照时间顺序只给出
n
n
n条记录的情况下,对于一辆公交车能使用的票的数量一定非常稀疏,因为时间足足是记录的
10000
10000
10000倍!那就意味着如果以时间作为索引去查找,一辆公交车的可行票数一定只有一个很小很小,远远不到
n
/
2
n/2
n/2级别的范围,应该最多是常数
45
45
45,而且即使有几个范围都达到了局部密集(也实在是小),其余的范围一定非常非常小!那么,我们仍然是暴力查询这个范围的每张票,但是可以用什么操作高效得到时间范围。
什么操作?我最开始就想过的二分啊!
时间无非就是
b
u
s
i
.
t
i
−
45
busi.ti-45
busi.ti−45到超过
b
u
s
i
.
t
i
busi.ti
busi.ti的第一个位置。
二分查找,走着!
for(ll i=1;i<=len2;++i){
bool f=1;
//二分查找左端点
ll L=1,R=len1,mid=0,ans=0;
while(L<=R){
mid=(L+R)>>1;
if(sub[mid].ti+45>=bus[i].ti){
R=mid-1;
ans=mid;
}
else
L=mid+1;
}
ans=L;
//查找右端点
L=1,R=len1,mid=0;
ll ans1=0;
while(L<=R){
mid=(L+R)>>1;
if(sub[mid].ti>bus[i].ti){
R=mid-1;
ans1=mid;
}
else
L=mid+1;
}
//注意,因为右端点是第一张超过时间限制的票,所以不能用,要<L,而不是<=
for(ll j=ans;j<L;++j)//暴力查找范围
if(sub[j].pi>=bus[i].pi&&!vis[j]){
f=0;
vis[j]=1;
break;
}
if(f)
res+=bus[i].pi;
}
那么,现在给我的思路的完整代码,
T2.2 代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+10,M=1e3+20;
ll n,res,len1,len2;
struct node{
ll pi;
ll ti;
}sub[N],bus[N];
bool vis[N];
int main(){
//freopen("transfer.in","r",stdin);
//freopen("transfer.out","w",stdout);
//len1=50000,len2=50,000
//len2*2(loglen1)*45
scanf("%lld",&n);
ll op,p,t;//0 代表地铁,1 代表公交车
for(ll i=1;i<=n;++i){
scanf("%lld%lld%lld",&op,&p,&t);
if(op==0){
res+=p;
sub[++len1]={p,t};
}
else
bus[++len2]={p,t};
}
for(ll i=1;i<=len2;++i){
bool f=1;
// for(ll j=1;j<=len1;++j){//所有的地铁票
// if(sub[j].ti>bus[i].ti)//小小优化一下
// break;
// if(sub[j].pi>=bus[i].pi&&bus[i].ti-sub[j].ti<=45&&!vis[j]){//满足两个条件,同时没用
// vis[j]=1;
// f=0;//这个是标记
// break;//找到就停
// }
// }
//二分查找做断电
ll L=1,R=len1,mid=0,ans=0;
while(L<=R){
mid=(L+R)>>1;
if(sub[mid].ti+45>=bus[i].ti){
R=mid-1;
ans=mid;
}
else
L=mid+1;
}
// cout<<L<<" "<<R<<endl;
ans=L;
//查找右端点
L=1,R=len1,mid=0;
ll ans1=0;
while(L<=R){
mid=(L+R)>>1;
if(sub[mid].ti>bus[i].ti){
R=mid-1;
ans1=mid;
}
else
L=mid+1;
}
// cout<<L<<" "<<R<<endl;
// puts("");
// cout<<ans<<" "<<ans1<<endl;
//注意,因为右端点是第一张超过时间限制的票,所以不能用,要<L,而不是<=
for(ll j=ans;j<L;++j)//暴力查找范围
if(sub[j].pi>=bus[i].pi&&!vis[j]){
f=0;
vis[j]=1;
break;
}
if(f)
res+=bus[i].pi;
}
printf("%lld\n",res);
return 0;
}
/*
6
0 10 3
0 12 50
0 5 110
1 5 46
1 3 96
1 6 135
6
0 5 1
0 20 16
0 7 23
1 18 31
1 4 38
1 7 68
*/
那么这个时间复杂度的极限就是
O
(
n
/
2
∗
2
∗
(
l
o
g
n
/
2
)
∗
45
)
O(n/2*2*(log\ n/2)*45)
O(n/2∗2∗(log n/2)∗45),当然不是每个区间都
45
45
45,但是这里直接算了。大概是
1
0
7
10^7
107的样子,侥幸过了。
而我人傻了,看了题解之后
T2.3 正解思路
这玩意几个上次开始的位置不就完了!!!!我二分什么啊啊啊啊啊!!!!!!
正解就是记录上一次的位置,然后到当前票的位置就完事了。甚至还有更暴力的,直接看前
45
45
45张票,这样的时间也差不多。
我不想写了……我实在是********
T3 纪念品(souvenir)
T3.1 思路处理
这题放眼望去,没辙了,肯定得DP。
然后我就寄了。
我一下子把自己完全绕了进去,完全不知道该设什么状态。
那么现在,我们一步一步来。
先把题目中的信息理一下:
1、纪念品的当日价格既是花费金币数,也是卖出的所得金币数
2、卖出或买入的操作都可以进行无限次
3、买入的纪念品可以立即卖出用来继续买,也可以一直持有
4、最后一天将手里有的纪念品全卖掉,求已有金币数+卖掉金币数的最大值
我们需要注意这个“立即”,因为这给了我们第一条结论:
结论1:题目中所说的当天立即卖就是鬼扯,因为那样等价于不买
然后我们在结论1的基础上可以再往下进行推论,得到结论2
结论2:因为当天买,卖相当于不买,所以我们可以拉长时间,第一天买进来,第二天,第三天都可以当天卖,买。对上第三条要求中的“一直持有”
有了这个结论,我们就将“一直持有”变形了,这个状态就简便成了:第一天买,第二天卖,效果是一样的。
然后我们注意这个当天买,明天卖,我们即将推出这个DP最难想到的点。
是不是明天卖相当于获得了一定数量的金币,这是不是意味着我们花费了今天的金币数量能获得明天的金币数量?那么对于这一个操作,我们需要消耗今天金币数,获得价值为明天金币数
同样是因为这个结论,我们可以在每天一开始的时候把手里有的纪念品全部卖掉(刚刚已经推论过等价),那么这时我们已有资本就是这时的金币数
好了,我们再来看看这三个关键词:消耗、价值、已有资本
对应一下:体积,价值,背包大小
好了,那么我们已经可以断定这题是个背包了,而且背包的几个要素都知道了。
接下来,我们来看看背包类型,以及具体的实现。
首先因为每一天都是相对独立的,所以我们不妨把每一天先分开看,这里面的背包就和刚刚分析的一样,然后因为买卖无限次,所以做个完全背包就好了。那么我们不妨先按着“问什么,设什么”的方式,来推到一下这个转移方程
我们设状态为"
d
p
[
]
dp[\ ]
dp[ ]",
d
p
[
i
]
dp[i]
dp[i]表示花费为
i
i
i个金币时最大收益(明天早上卖掉能赚得最大金币数),那么对于每一种物品,我们就可以直接背模板:
d
p
[
i
]
=
m
a
x
(
d
p
[
i
]
,
d
p
[
i
−
p
r
i
c
e
[
t
]
[
j
]
]
+
p
r
i
c
e
[
t
+
1
]
[
j
]
−
p
r
i
c
e
[
t
]
[
j
]
)
dp[i]=max(dp[i],dp[i-price[t][j]]+price[t+1][j]-price[t][j])
dp[i]=max(dp[i],dp[i−price[t][j]]+price[t+1][j]−price[t][j])
这里的
p
r
i
c
e
[
t
]
[
j
]
price[t][j]
price[t][j]表示第
t
t
t天,第
j
j
j种物品的价格。
好,我们把DP部分的伪代码拎出来:
循环枚举天数{
枚举每种物品 {
枚举可能价格(完全背包,正着)
dp[i]=max(dp[i],dp[i-price[t][j]]+(price[t+1][j]-price[t][j]));
}
m+=max(0,dp[m]);//加上新获得的最大钱数
}
因为每一天单独做,所以我们可以得到的是每一天的最大收益,这个收益加上原有钱数,才是我们第二天能花费的最大钱数,这样才进入第二天。
最后瞅一眼数据范围"
T
≤
100
,
N
≤
100
,
M
≤
1
0
3
T≤100,N≤100,M≤10^3
T≤100,N≤100,M≤103",没问题,三重循环,一重天数,两重跑完全背包。时间
O
(
N
3
)
O(N^3)
O(N3)
T3.2 代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e2+10,M=1e4+10;
ll t,n,m;
ll a[N][N],dp[M];
int main(){
//freopen("souvenir.in","r",stdin);
//freopen("souvenir.out","w",stdout);
scanf("%lld%lld%lld",&t,&n,&m);
for(int i=1;i<=t;++i)//输入
for(int j=1;j<=n;++j)
scanf("%lld",&a[i][j]);
for(int i=1;i<=t;++i){//枚举天数
memset(dp,0,sizeof dp);
/*
因为每一天独立做背包,所以记得清空数组
*/
for(int j=1;j<=n;++j)//完全背包模板
for(int k=a[i][j];k<=m;++k)
dp[k]=max(dp[k],dp[k-a[i][j]]+a[i+1][j]-a[i][j]);
//注意,这里背包的价值不完全是分析的“价值”,背包里算的是差价,那才是赚的钱
m+=max((ll)0,dp[m]);//加上新的钱数,进入下一天
}
printf("%lld\n",m);//输出最大钱数
return 0;
}
T4 加工零件(work)
T4.1 思路处理
真的,有的题目看上去很难,你看上去完全没有办法的时候,再推一下,再多想一下,就出来了。
这题暴力我当时没敢打,直接就奔着满分去了。因为觉得这题挺简单的,实际上最暴力的递归也有
40
40
40~
60
60
60分了,甚至做得好理论山可以达到
80
80
80。但是我就是因为觉得简单,然后就不想放。
事实上也确实简单:我们直接看几个样例,画一下图,可以得到最基础的推论:
推论1:如果点 1 1 1到点 i i i中存在长度为 x x x的路径,那么如果第 i i i号工人如果生产阶段 x + k x x+kx x+kx( k k k为偶数),肯定要提供原料
然后我就懵了,
1
1
1到其他点的路径长度很多,按照我这个操作,需要求出很多不同的路径,那时间就寄了。
然后我就放弃了。
实际上,如果我一个一个阶段在图上模拟,很容易得出更普遍的结论,我们这里把它称为定理吧。
定理:如果点 1 1 1到点 i i i中存在长度为 x x x的路径,那么如果第 i i i号工人如果生产阶段 x + 2 k x+2k x+2k,肯定要提供原料
没太看懂?我们画个图验证一下:
看到了吗?只要先走完这个点到点
1
1
1的路径,然后就在和点
1
1
1相邻的点那里“反复横跳”就行了。
所以,这题就变成了求最短路径的问题,稍稍有一点变形。
我们需要注意到:这样长度为
3
3
3(奇数)的路径里是管不着偶数的,所以我们不妨求出点
1
1
1到每一个点的“奇数最短路径+偶数最短路径”,然后只要判断这个阶段是奇数还是偶数,是否能够来回跳就行了。
然后就是简单的发指的最短路径,各位自己看注释,这题完全可以从绿降黄。
T4.2 代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+10,INF=0x7ffffff;
ll n,m,q,id=0,head[N*2],dis[N][2];//dis[i][0]记录最短偶数路径,dis[i][1]记录最短技术路径
bool vis[N][2];
struct node{//前向星
ll v;
ll nxt;
node(){
nxt=0;
}
node(ll a,ll b){
v=a;
nxt=b;
}
}g[N*2];
void add_edge(ll u,ll v){
g[++id]=node(v,head[u]);
head[u]=id;
}
void SPFA(){//SPFA
for(ll i=1;i<=n;++i)//初始化为无穷大
dis[i][1]=dis[i][0]=INF;
vis[1][0]=1;//点1偶数路径存在于队列
dis[1][0]=0;
queue<pair<ll,ll> >qu;//注意,这里要记录两个信息:点,奇偶性
qu.push(make_pair(1,0));
while(!qu.empty()){
ll f=qu.front().first;
ll ty=qu.front().second;
vis[f][ty]=0;//点f的ty性路径不在队列
qu.pop();
for(ll i=head[f];i;i=g[i].nxt){
ll v=g[i].v;
if(dis[v][0]>dis[f][1]+1){//这个点的偶数路径长度=源点(f)的奇数路径长度+1
dis[v][0]=dis[f][1]+1;
if(!vis[v][0]){//是否存在?
qu.push(make_pair(v,0));//不存在入队
vis[v][0]=1;//标记
}
}
if(dis[v][1]>dis[f][0]+1){//奇数=偶数+1
dis[v][1]=dis[f][0]+1;
if(!vis[v][1]){
qu.push(make_pair(v,1));
vis[v][1]=1;
}
}
}
}
}
int main(){
//freopen("work.in","r",stdin);
//freopen("work.out","w",stdout);
scanf("%lld%lld%lld",&n,&m,&q);
ll u,v;
for(ll i=1;i<=m;++i){
scanf("%lld%lld",&u,&v);
add_edge(u,v);
add_edge(v,u);
}
SPFA();
if(head[1]==0)//如果点1没有连边,那么不存在偶数路径
dis[1][0]=INF;
ll a,L;
for(int i=1;i<=q;++i){
scanf("%lld%lld",&a,&L);
if(L>=dis[a][L%2])//如果这个长度>=最短x性长度,那么就可以靠反复横跳提供原料
printf("Yes\n");
else//不然不用管
printf("No\n");
}
return 0;
}
总结——
我的确是生病着,但是这不是理由。
第一题几分钟就过了,第二题居然耗了将近一小时,我容易把问题想的很复杂,但这就表明我对已有知识不够熟练(试想一下,要是那道题是
1
0
6
10^6
106,我的二分还能过吗?)。
第三题和第四题按道理我都是可以轻松过的,尤其是第四题,做最后两题的时候先开的是第四题,但是因为最开始模拟样例没有挨个模拟,就导致了我的错误推论,然后就写不出来正解。
实际上,我即使不写正解,完全也可以暴力拿分,然后对拍就能发现规律了。但是我还是一开始就定了过高的目标,导致自己在错误之下无法调整。
更无语的是:第四题知道正解之后,我不会写最短路了!这种情况上考场——“哎,我都会,但是不会写”……
第三题的DP确实是有一定思维难度,容量、体积和价值的推导的确是很难想——但是我可以贪心暴力啊!这样,理论上说,我的分数应该是
300
+
300+
300+,可实际只得了
200
200
200。