【算法题解】部分洛谷题解(上)

前言

本篇为我做过的洛谷题的部分题解,大多是我认为比较具有代表性的或者比较有意思的题目,包含我自己的思考过程和想法。


[NOIP2001 提高组] 数的划分

题目描述

将整数 n n n 分成 k k k 份,且每份不能为空,任意两个方案不相同(不考虑顺序)。

例如: n = 7 n=7 n=7 k = 3 k=3 k=3,下面三种分法被认为是相同的。

1 , 1 , 5 1,1,5 1,1,5;
1 , 5 , 1 1,5,1 1,5,1;
5 , 1 , 1 5,1,1 5,1,1.

问有多少种不同的分法。

输入格式

n , k n,k n,k 6 < n ≤ 200 6<n \le 200 6<n200 2 ≤ k ≤ 6 2 \le k \le 6 2k6

输出格式

1 1 1 个整数,即不同的分法。

样例 #1

样例输入 #1

7 3

样例输出 #1

4

提示

四种分法为:
1 , 1 , 5 1,1,5 1,1,5;
1 , 2 , 4 1,2,4 1,2,4;
1 , 3 , 3 1,3,3 1,3,3;
2 , 2 , 3 2,2,3 2,2,3.

【题目来源】

NOIP 2001 提高组第二题


解题思路

其是这题我一看下去觉得纯纯搜索就完了,就直接采用了DFS的思路,考虑一下递归边界,做一个可行性减枝,一顿搜索后就是60分。更换思路,改成从n-k+11循环,从后往前遍历i,觉得能够减少循环,把大的数字先找出来,是80分。

#include<cstdio>
using namespace std;
int ans=0,n,k;
void dfs(int sum,int begin,int num){
	if(sum>n || num >k) return;
	if(sum==n && k==num){
		ans++;
		return;
	}
	//for(int i=begin;i<=n-1;i++) //60分,未优化
	for(int i=begin;i>=1;i--)     //80分 
		dfs(sum+i,i,num+1);		
}
int main(){
	scanf("%d%d",&n,&k);
	dfs(0,n-k+1,0);
	printf("%d",ans);
	return 0;
} 

在这里插入图片描述

仔细思考后,发现原来是边界条件搞错了。。。

修改后就是100分了。

#include<cstdio>
using namespace std;
int ans=0,n,k;
void dfs(int sum,int begin,int num){
	if(sum>n || num >k) return;
	if(k==num){
		if(sum==n)
			ans++;
		return;
	}
	for(int i=begin;i>=1;i--)
		dfs(sum+i,i,num+1); 
}
int main(){
	scanf("%d%d",&n,&k);
	//dfs(0,1,0);
	dfs(0,n-k+1,0);
	printf("%d",ans);
	return 0;
} 

其实这道题还有一种算法思路————动态规划。

f[i][x] 表示 i 分成 x 个非空的数的方案数。

显然 i<x 时 f[i][x]=0 , i=x 时 f[i][x]=1;

其余的状态,我们分情况讨论:

①有1的 ②没有1的

第一种情况,方案数为 f[i-1][x-1]

第二种情况,方案数为 f[i-x][x] (此时 i 必须大于 x)

所以,状态转移方程为: f[i][x]=f[i-1][x-1]+f[i-x][x]

程序如下:

#include<bits/stdc++.h>
using namespace std;

int main() {
    int n, k;
    cin >> n >> k;
    int f[201][7]; // f[k][x] k 分成 x 份 ={f[k-1][x-1],f[k-x][x]}
    
    // 初始化边界条件
    for (int i = 1; i <= n; i++) {
        f[i][1] = 1;
        f[i][0] = 1;
    }
    for (int x = 2; x <= k; x++) {
        f[1][x] = 0;
        f[0][x] = 0;
    }

    // 动态规划求解
    for (int i = 2; i <= n; i++) {
        for (int x = 2; x <= k; x++) {
            if (i > x) {
                f[i][x] = f[i - 1][x - 1] + f[i - x][x];
            } else {
                f[i][x] = f[i - 1][x - 1];
            }
        }
    }
    
    // 输出结果
    cout << f[n][k];
    
    return 0;
}

奇怪的电梯

题目背景

感谢 @yummy 提供的一些数据。

题目描述

呵呵,有一天我做了一个梦,梦见了一种很奇怪的电梯。大楼的每一层楼都可以停电梯,而且第 i i i 层楼( 1 ≤ i ≤ N 1 \le i \le N 1iN)上有一个数字 K i K_i Ki 0 ≤ K i ≤ N 0 \le K_i \le N 0KiN)。电梯只有四个按钮:开,关,上,下。上下的层数等于当前楼层上的那个数字。当然,如果不能满足要求,相应的按钮就会失灵。例如: 3 , 3 , 1 , 2 , 5 3, 3, 1, 2, 5 3,3,1,2,5 代表了 K i K_i Ki K 1 = 3 K_1=3 K1=3 K 2 = 3 K_2=3 K2=3,……),从 1 1 1 楼开始。在 1 1 1 楼,按“上”可以到 4 4 4 楼,按“下”是不起作用的,因为没有 − 2 -2 2 楼。那么,从 A A A 楼到 B B B 楼至少要按几次按钮呢?

输入格式

共二行。

第一行为三个用空格隔开的正整数,表示 N , A , B N, A, B N,A,B 1 ≤ N ≤ 200 1 \le N \le 200 1N200 1 ≤ A , B ≤ N 1 \le A, B \le N 1A,BN)。

第二行为 N N N 个用空格隔开的非负整数,表示 K i K_i Ki

输出格式

一行,即最少按键次数,若无法到达,则输出 -1

样例 #1

样例输入 #1

5 1 5
3 3 1 2 5

样例输出 #1

3

提示

对于 100 % 100 \% 100% 的数据, 1 ≤ N ≤ 200 1 \le N \le 200 1N200 1 ≤ A , B ≤ N 1 \le A, B \le N 1A,BN 0 ≤ K i ≤ N 0 \le K_i \le N 0KiN

本题共 16 16 16 个测试点,前 15 15 15 个每个测试点 6 6 6 分,最后一个测试点 10 10 10 分。


这道题也可以多解,我们刚拿到题目的时候,肯定会直接思考模拟然后搜索,这题就可以用dfs或bfs来做。

#include<bits/stdc++.h>
using namespace std;
int n,a,b,k[201],dis[201];
void dfs(int node,int step){
	dis[node]=step;//一定可以更新
	int v=node-k[node];
	if(1<=v&&step+1<dis[v]/*可以更新在搜索*/)//下
		dfs(v,step+1);
	v=node+k[node];
	if(v<=n&&step+1<dis[v])//上
		dfs(v,step+1);
	return;
}
int main(){
	memset(dis,0x3f,sizeof(dis));
	cin>>n>>a>>b;
	for(int i=1;i<=n;i++)
		cin>>k[i];
	dfs(a,0);
	cout<<(dis[b]==0x3f3f3f3f?-1:dis[b]);
	return 0;
}

注: 这个0x3f3f3f3f是个经验数字,他略小于int的最大值2^31-1的一半

这题第二个思路就是————最短路径

有没有很吃惊!我们可以把每一层楼抽象成图中的节点,然后构建邻接矩阵。然后随便写个Floyd算法就行了(因为本题的数据量比较小,而Floyd算法最简单,五行代码,可以用此方法大材小用)

#include<cstdio>
#define inf 99999999
using namespace std;
int main(){
	int n,a,b,t,i,j,k;
	int e[201][201];
	scanf("%d%d%d",&n,&a,&b);
	for(i=1;i<=n;i++)
		for(j=1;j<=n;j++)
			if(i==j) e[i][j]=0;
			else e[i][j]=inf;
	for(int i=1;i<=n;i++){
		scanf("%d",&t);
		if(t+i<=n) e[i][t+i]=1;
		if(i-t>=1) e[i][i-t]=1;
	}
	for(k=1;k<=n;k++)
		for(i=1;i<=n;i++)	
			for(j=1;j<=n;j++)
				if(e[i][j]>e[i][k]+e[k][j])
					e[i][j]=e[i][k]+e[k][j];
	int ans=e[a][b];
	if(ans==inf) printf("-1");
	else printf("%d",ans);
	return 0;				
} 

[NOIP2004 提高组] 合并果子 / [USACO06NOV] Fence Repair G

题目描述

在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。

每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过 n − 1 n-1 n1 次合并之后, 就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。

因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为 1 1 1 ,并且已知果子的种类 数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。

例如有 3 3 3 种果子,数目依次为 1 1 1 2 2 2 9 9 9 。可以先将 1 1 1 2 2 2 堆合并,新堆数目为 3 3 3 ,耗费体力为 3 3 3 。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为 12 12 12 ,耗费体力为 12 12 12 。所以多多总共耗费体力 = 3 + 12 = 15 =3+12=15 =3+12=15 。可以证明 15 15 15 为最小的体力耗费值。

输入格式

共两行。
第一行是一个整数 n ( 1 ≤ n ≤ 10000 ) n(1\leq n\leq 10000) n(1n10000) ,表示果子的种类数。

第二行包含 n n n 个整数,用空格分隔,第 i i i 个整数 a i ( 1 ≤ a i ≤ 20000 ) a_i(1\leq a_i\leq 20000) ai(1ai20000) 是第 i i i 种果子的数目。

输出格式

一个整数,也就是最小的体力耗费值。输入数据保证这个值小于 2 31 2^{31} 231

样例 #1

样例输入 #1

3 
1 2 9

样例输出 #1

15

提示

对于 30 % 30\% 30% 的数据,保证有 n ≤ 1000 n \le 1000 n1000

对于 50 % 50\% 50% 的数据,保证有 n ≤ 5000 n \le 5000 n5000

对于全部的数据,保证有 n ≤ 10000 n \le 10000 n10000


这题也有两种方法,总体都为贪心。

第一个方法其实就是上课天天讲的哈夫曼模版题,我这里没有直接使用C++的STL中的优先队列,手搓一个小根堆。时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

#include<cstdio>
using namespace std;

int h[20010],n;
void swap(int x,int y){
	int t=h[x];
	h[x]=h[y];
	h[y]=t;
}
void siftdown(int i){
	int t,flag=0;
	while(i*2<=n && flag==0){
		if(h[i]>h[i*2])
			t=i*2;
		else
			t=i;
		if(i*2+1<=n)
			if(h[t]>h[i*2+1])
				t=i*2+1;
		if(t!=i){
			swap(t,i);
			i=t;
		}else
			flag=1;				
	}
	return ;
}
void creat(){
	for(int i=n/2;i>=1;i--)
		siftdown(i);
}
int main(){
	int num;
	scanf("%d",&num);
	//printf("ee\n");
	for(int i=1;i<=num;i++)
		scanf("%d",&h[i]);
	
	n=num;
	creat();       //建最小堆
	 
//	for(int i=1;i<=n;i++)
//		printf("%d ",h[i]);
//	printf("\n");
	
	int ans=0;
	
	for(int i=1;i<num;i++){
		ans+=h[1];
		if(n>=3 && h[3]<=h[2]){
				ans+=h[3];
				h[3]+=h[1];	
				siftdown(3);
			}
		else	
		if((n>=3 && h[2]<=h[3]) || n==2){
				ans+=h[2];
				h[2]+=h[1];	
				siftdown(2);
			}	
		h[1]=h[n--];
		siftdown(1);
//		for(int j=1;j<=n;j++)
//			printf("%d ",h[j]);
//		printf("\n");	
//		printf("ans=%d\n",ans);
	}
	
//	printf("sum=%d\n",sum);	
	printf("%d",ans);		
	return 0;
}

第二种方法是建立两个数组,第一个数组存储每堆果子的重量并从小往大排序。从第一个数组中取出前两个就是最小的两堆果子。把这两堆果子取出(从数组中划掉)合并一次成为新的一堆,记录消耗的体力,然后把这两堆果子的总和放在第二的数组后面。接下来还要用同样的方法找到最小的另一堆,合并,也放在第二个数组中,这两个数组都是从小往大排序的,所以两个数组中最小的那一堆一定就在两个数组没有被划掉的元素的最头部。重复这样的操作,直到最后两堆果子被合并。

在这里插入图片描述

这种算法时间复杂度还是O( n l o g n nlogn nlogn),取决于排序的耗时。

#include<cstring>
using namespace std;
#define maxn 20010
#define MAX 127
int main(){
	int n,a[maxn],b[maxn],m=0;
	scanf("%d",&n);
	memset(a,MAX,sizeof(a));
	memset(b,MAX,sizeof(b));
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	sort(a+1,a+n+1);
	// 注意a+1,a+n+1; 
	int i=1,j=1,ans=0;
	for(int k=1;k<n;k++){
		int w1=a[i]<b[j] ? a[i++] : b[j++];
		int w2=a[i]<b[j] ? a[i++] : b[j++];
//		printf("w1=%d\n",w1);
//		printf("w2=%d\n",w2);
		b[++m]=w1+w2;
		ans+=w1+w2;
	}	
	printf("%d",ans);
	return 0;
}

[NOIP1999 提高组] 导弹拦截

题目描述

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

输入导弹依次飞来的高度,计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

输入格式

一行,若干个整数,中间由空格隔开。

输出格式

两行,每行一个整数,第一个数字表示这套系统最多能拦截多少导弹,第二个数字表示如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

样例 #1

样例输入 #1

389 207 155 300 299 170 158 65

样例输出 #1

6
2

提示

对于前 50 % 50\% 50% 数据(NOIP 原题数据),满足导弹的个数不超过 1 0 4 10^4 104 个。该部分数据总分共 100 100 100 分。可使用 O ( n 2 ) \mathcal O(n^2) O(n2) 做法通过。
对于后 50 % 50\% 50% 的数据,满足导弹的个数不超过 1 0 5 10^5 105 个。该部分数据总分也为 100 100 100 分。请使用 O ( n log

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值