NOIP2004提高组题解

本文深入解析了四道经典算法题目,包括模拟、贪心、动态规划和搜索剪枝等核心算法,提供了详细的代码实现及剪枝技巧,强调了严谨性和细节的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

T1:津津的储蓄计划

考察知识:模拟

算法难度:X 实现难度:X+

分析:按照题目的要求模拟就可以了,只是要考虑严谨,还要看懂题目

代码:

#include<cstdio>
int cost,rest,store,fail;
int main(){
	for(int i=1;i<=12;i++){
		scanf("%d",&cost);
		rest+=(300-cost);
		if(rest<0) {fail=i;break;}
		else store+=120*(rest/100),rest%=100;
	}
	if(fail) printf("%d\n",-fail);
	else printf("%d\n",store+rest);
	return 0;
}

T2:合并果子

考察知识:二叉堆,模拟,贪心

算法难度:XX 实现难度:XX+

分析:

直接每次合并最小的两堆就可以了

还是那句话:当年不能用STL,所以我们还是按当年的要求来吧(不用优先队列)

用priority_queue很方便,就不贴代码了,下面的代码是用二叉堆实现的,注释里面讲述了使用方法,你需要有二叉树的的知识储备才容易读懂

代码:

#include<cstdio>
#include<algorithm>
const int maxn=10005;
int heap[maxn],np,n,t,sum;//维护一个小根堆
/*heap是储存二叉树的一个数组,2*i,2*i+1是i的
左右儿子,保证这棵二叉树父亲的权值不比儿子大*/ 
void add(int x){//添加元素 
	heap[++np]=x;//把np理解为数组的指针-指向末尾元素
	for(int i=np,j=i/2;j>0;i=j,j/=2){//调整二叉树的节点权值 
		if(heap[j]>heap[i]) std::swap(heap[i],heap[j]);
		else break;
	}
}
void del(){//删除最小元素 
	heap[1]=heap[np--];//把根赋最后节点的值,删除最后节点
	for(int i=1,j=2*i;j<=np;i=j,j*=2){//调整二叉树 
		if(j<np&&heap[j+1]<heap[j]) j++;
		if(heap[i]>heap[j]) std::swap(heap[i],heap[j]);
		else break;
	} 
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&t),add(t);
	while(--n){
		t=heap[1],del();
		t+=heap[1],del();
		add(t),sum+=t;
	}
	printf("%d\n",sum); 
	return 0;
}

T3:合唱队形

考察知识:序列型动态规划

算法难度:XX+ 实现难度:XX+

分析:

其实就是求一次最长上升子序列,然后反着求一次最长下降子序列,然后找最高点就可以了

代码:

#include<cstdio>
#include<algorithm>
int n,a[105],f1[105],f2[105],mx;
//f1(i)=max(f1(j))+1||1<=j<i a[j]<a[i]
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",a+i),f1[i]=f2[i]=1;
	
	for(int i=2;i<=n;i++)
	  for(int j=1;j<i;j++) if(a[j]<a[i])
	    f1[i]=std::max(f1[i],f1[j]+1);
	
	for(int i=n-1;i>=1;i--)
	  for(int j=n;j>i;j--) if(a[i]>a[j])
	    f2[i]=std::max(f2[i],f2[j]+1);
	
	for(int i=1;i<=n;i++) mx=std::max(mx,f1[i]+f2[i]-1);
	printf("%d\n",n-mx);
	return 0;
}

T4:虫食算

考察知识:搜索+剪枝,数学,高斯消元

算法难度:XXXX 实现难度:XXXX

分析:这道题搜索并不难写,但是剪枝的程度决定了你能得多少分

我第一次花了两个小时写(调bug)完了第一份程序,如果不剪枝,时间复杂度为:O(n^2^n),可是我加了剪枝也才20分,然后我修改了一下,时间复杂度开了个根号,然后就可以90分了

下面先介绍90分方法:

首先是命名方式:

add[i]表示第i+1为应该向第i为进一

剪枝:1.如果当前某个字母必须赋某个值而这个值已经被使用,剪枝 2.如果某个字母已经被赋值了,就不用再枚举其可能的值了

其实剪枝并不是很多,也不是很复杂,只不过细节还是很多,很容易写错,情况要分类讨论(我找bug又花了一个小时T_T)

所以忠告大家一定要加强静态查错,考虑每一个可能出错的地方

代码:

#include<iostream>
#include<cstdio>
int n,mp[30],used[30],add[30],got_ans;
char A[30],B[30],C[30];
void dfs(int row,int cur){
    if(row<0){
        if(!add[row+1])//第一位不该有进位 
            for(int i=0;i<n;i++) printf("%d ",mp[i]);
        got_ans=1;return;
    }
    if(got_ans) return;
    int idA=A[row]-'A',idC=C[row]-'A';
    if(cur) goto Cur1;
    //考虑第row列第一行的字母代表的数字: 
    if(mp[idA]>=0) {dfs(row,1);return;}//如果字母已经被赋值 
    for(int i=0;i<n;i++) if(!used[i]){//枚举可以赋的值 
    	mp[idA]=i,used[i]=1;
    	dfs(row,1);
    	mp[idA]=-1,used[i]=0;//修改回原状态 
    }return;
    Cur1://考虑第row列第二行:
    int idB=B[row]-'A';
    if(mp[idB]>=0){//如果字母二已被赋值,分类讨论 
        int t=mp[idA]+mp[idB]+add[row+1];
        add[row]=t/n;//注意进位 
        if(mp[idC]>=0&&t%n==mp[idC]){//如果字母三已被赋值
            dfs(row-1,0);
            add[row]=0;
        }
        else if(mp[idC]<0&&used[t%n]==0){//根据字母一二可以确定字母三 
            mp[idC]=t%n,used[t%n]=1;
            dfs(row-1,0);
            mp[idC]=-1,add[row]=used[t%n]=0;
        }
        return;
    }
    int t;
    for(int i=0;i<n;i++) if(!used[i]){//枚举第字母二可能的值 
        t=mp[idA]+i+add[row+1];
        add[row]=t/n;
        mp[idB]=i,used[i]=1;
        if(mp[idC]>=0&&(t%n==mp[idC])) dfs(row-1,0);
        else if(mp[idC]<0&&used[t%n]==0){
            mp[idC]=t%n,used[t%n]=1;
            dfs(row-1,0);
            mp[idC]=-1,used[t%n]=0;
        }
        mp[idB]=-1,add[row]=used[i]=0;
    }
}
int main(){ 
    scanf("%d%s%s%s",&n,A,B,C);
    for(int i=0;i<n;i++) mp[i]=-1;
    dfs(n-1,0);
    return 0;
}

其实只要再加一个剪枝就是100分算法了:3.考虑剩下的数,如果一列的三个数值都知道了那么就可以判断是否矛盾了

但是要考虑进位(true表示可以剪枝):

bool check(int limt){
	int a,b,c;
	for(int i=0;i<=limt;i++){
		a=mp[A[i]-'A'],b=mp[B[i]-'A'],c=mp[C[i]-'A'];
		//考虑要严谨 
		if(((a+b)%n)!=c&&((a+b+1)%n)!=c&&a>=0&&b>=0&&c>=0) return true; 
	}
	return false;
}

最后修改一下:

    if(got_ans||check(row)) return;

就可以AC了,那个之前超时的点500ms过,还是有点悬,但是比起以前速度快了60多倍

总结:

不知道你有没有注意到所有代码我都没有用using namespace std;

如果你不知道为什么的话——解释:如果因为你不加这句话如果某些地方报错你在它(函数等)前面加  std::  一般就可以了

T1:刚学竞赛一个月就AC了,最近来做没有考虑严谨居然80分T_T

T2:第一次做出现在考试中,那时没有学优先队列,用map做的,100分,现在用二叉堆做,不小心写错一个字母(i写成了j)只得了20分T_T

T4:第一次同时搜索第一行,第二行,超时非常严重(有80分超时),搜索方法略微改变了一下,剪枝没有改就90了(但是找bug花了我很多时间),这说明了什么?:细节,严谨,前瞻性很重要。。。

总之这一年的题我做得非常浮躁,20+80+100+20=220,如果NOIP2018我还这么马虎,我就完了。。。

### NOIP 2017 提高 初赛题目解析 #### Pascal 编程语言的淘汰时间 关于 Pascal 编程语言何时不再被支持的问题,在选项中给出的时间点为 2022 年[^1]。 #### 归并算法最坏情况下的比较次数 对于两个长度均为 n 的有序数 A 和 B 进行归并操作时,如果仅考虑元素之间的比较作为基本运算,则在最坏的情况下,至少需要执行 \(2n - 1\) 次比较才能完成整个过程[^2]。 ```python def merge_sorted_arrays(A, B): merged_array = [] i, j = 0, 0 while i < len(A) and j < len(B): if A[i] <= B[j]: merged_array.append(A[i]) i += 1 else: merged_array.append(B[j]) j += 1 # Append any remaining elements from either list merged_array.extend(A[i:]) merged_array.extend(B[j:]) return merged_array ``` 此代码展示了如何有效地将两个已排序列表合并成一个新的已排序列表,并且在这个过程中进行了必要的比较来决定哪个元素应该先加入到新的列表当中去。当处理到最后一个元素的时候,实际上已经完成了 \(2n-1\) 次比较(假设两部分都含有 n 个不同大小的数),这正好对应于上述提到的最佳下界理论值。 #### 硬币分堆与查找不合格品的方法 给定一数量为 n 枚相同外观但可能有一枚质量不同的硬币成的集合 A ,通过天平称重的方式找出那枚与众不同的假币: 1. 计算 k 值等于向下取整后的 \(\lfloor{n / 3}\rfloor\); 2. 把这些硬币分为三份 X、Y 和 Z,每一份分别有 k 枚; 3. 如果 X 和 Y 的质量不相等的话,那么继续下一步骤;否则跳转至第 5 步; 4. 对较轻的那一侧进一步分割重复以上步骤直到找到唯一可疑对象为止; 5. 当剩下不超过两枚硬币时直接对比即可判断哪一个是异常者;而只有一枚的情况则默认其就是那个特殊个体[^3]. 这种策略利用了三分法的思想,每次都将待测样本缩小三分之一左右的比例,从而提高了效率同时也简化了解决方案的设计难度.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值