真题1:CSP-J 2019

感受——

  能有什么感受!?我真的不好骂自己的!
  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.ti45到超过 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/22(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[iprice[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 T100,N100,M103",没问题,三重循环,一重天数,两重跑完全背包。时间 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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值