ACMer必看的基础算法(附经典例题)

文章目录

  • 一、排序算法
    • 1.冒泡排序
    • 2.选择排序
    • 3.快速排序
    • 4.桶排序
  • 二、递归算法
  • 三、递推算法
  • 四、贪心算法
  • 五、动态规划
    • 基本模型
    • 区间dp
    • 背包问题
      • 01背包
      • 完全背包
      • 多重背包
  • 六、分治算法
    • 二分
    • 三分
  • 七、补充
    • STL的简单应用
    • 结构体

一、排序算法

1.冒泡排序

【基本思想】
基本思想为两两比较相邻记录的关键码,若反序则交换,直到没有反序为止。
冒泡排序的实现依靠,双重循环,外层 i 控制进行多少轮,内层循环 j 控制每轮比较的次数,对于 n 个元素,共进行 n-1 趟交换,每趟比较 n-i 次。
【排序过程】
具体的排序过程为:

  • 将整个待排序的序列分为有序区和无序区,初始时有序区为空,无序区包含所有待排序记录
  • 对无序区从前向后依次将相邻记录的关键码进行比较,若反序则交换,从而使得关键码小的记录前移,关键码大的记录后移
  • 重复执行步骤 2,直到无序区没有反序记录

实例

初始关键字: 『 6,5,3,1,8,7,2,4 』

第一趟排序: 『6,5,3,1,8,7,2,4 』

                      5,『 6,3,1,8,7,2,4 』

                      5,3,『 6,1,8,7,2,4 』

                      5,3,1,『 6,8,7,2,4 』

                      5,3,1,6,『 8,7,2,4 』

                      5,3,1,6,7,『 8,2,4 』

                      5,3,1,6,7,2,『 8,4 』

                      5,3,1,6,7,2,4,『 8 』

第二趟排序: 『 5,3,1,6,7,2,4 』,8

                      3,『5,1,6,7,2,4』,8

                      3,1,『5,6,7,2,4』,8

                      3,1,5,『6,7,2,4』,8

                      3,1,5,6,『7,2,4』,8

                      3,1,5,6,2,『7,4』,8

                      3,1,5,6,2,4,『7』,8

第三趟排序: 『 3,1,5,6,2,4 』,7,8

                      1,『3,5,6,2,4』,7,8

                      1,3,『5,6,2,4』,7,8

                      1,3,5,『6,2,4』,7,8

                      1,3,5,2,『6,4』,7,8

                      1,3,5,2,4,『6』,7,8

第四趟排序: 『 1,3,5,2,4 』,6,7,8

                      1,『3,5,2,4』,6,7,8

                      1,3,『5,2,4』,6,7,8

                      1,3,2,『5,4』,6,7,8

                      1,3,2,4,『5』,6,7,8

第五趟排序: 『 1,3,2,4 』,5,6,7,8

                      1,『3,2,4』,5,6,7,8

                      1,2,『3,4』,5,6,7,8

                      1,2,3,『4』,5,6,7,8

第六趟排序: 『 1,2,3 』,4,5,6,7,8

                      1,『2,3』,4,5,6,7,8

                      1,2,『3』,4,5,6,7,8

第七趟排序: 『 1,2 』,3,4,5,6,7,8

                    『 1 』,2,3,4,5,6,7,8

结果: 『 1,2,3,4,5,6,7,8 』

【时空复杂度】
平均时间复杂度为 O(n^2),空间复杂度为 O(1)
【代码实现】

void bubbleSort(int a[],int n)
{
    for(int i=1;i<=n-1;i++)//比较n-1趟
        for(int j=1;j<=n-i;j++)//每趟交换n-i次
            if(a[j]>a[j+1])
                swap(a[j],a[j+1]);
}

2.选择排序

【基本思想】
其基本思想是:第 i 趟排序再待排序序列 a[i]~a[n] 中选取关键码最小的记录,并和第 i 个记录交换作为有序序列的第 i 个记录。

其实现利用双重循环,外层 i 控制当前序列最小值存放的数组元素位置,内层循环 j 控制从 i+1 到 n 序列中选择最小的元素所在位置 k

【排序过程】
具体的排序过程为:

将整个记录序列划分为有序区和无序区,初始时有序区为空,无序区含有待排序的所有记录
在无序区选择关键码最小的记录,将其与无序区中的第一个元,使得有序区扩展一个记录,同时无序区减少了一个记录
不断重复步骤 2,直到无序区只剩下一个记录为止

实例
初始关键字:『 8,5,2,6,9,3,1,4,0,7 』

第一趟排序后:0,『5,2,6,9,3,1,4,8,7』

第二趟排序后:0,1,『2,6,9,3,5,4,8,7』

第三趟排序后:0,1,2,『6,9,3,5,4,8,7』

第四趟排序后:0,1,2,3,『9,6,5,4,8,7』

第五趟排序后:0,1,2,3,4,『6,5,9,8,7』

第六趟排序后:0,1,2,3,4,5,『6,9,8,7』

第七趟排序后:0,1,2,3,4,5,6,『9,8,7』

第八趟排序后:0,1,2,3,4,5,6,7,『8,9』

第九趟排序后:0,1,2,3,4,5,6,7,8,『9』

结果: 『 0,1,2,3,4,5,6,7,8,9 』

【时空复杂度】
时间复杂度为 O(n^2),空间复杂度为 O(1)

【代码实现】

void selectSort(int a[],int n){
    for(int i=1;i<=n-1;i++){//进行n-1趟选择
        int index=i;
        for(int j=i+1;j<=n;j++)//从无序区选取最小的记录
            if(a[index]>a[j])
                index=j;
        if(index!=i)
            swap(a[i],a[index]);
    }
}

3.快速排序

【基本思想】
快速排序是一种不稳定的排序方法,其同样属于交换排序,是对冒泡排序的一种改进:在冒泡排序中,记录的比较与移动是在相邻位置进行的,记录每次交换只能后移一个位置,因而总的比较次数与移动次数较多;而在快速排序中,记录的比较与移动是从两端向中间进行的,关键码较大的记录一次就能从前面移动到后面,关键码较小的记录一次就能从后面移动到前面,由于记录移动的距离较远,从而减少了总的比较次数与移动次数。

【实现过程】
快速排序利用了分治策略,其基本思想是:首先选一个轴值,作为比较的基准,将待排序记录划分为独立的两部分,左侧记录均小于等于轴值,右侧记录均大于等于轴值,然后分别对这两部分重复上述过程,直至序列有序。

显然,快速排序是一个递归的过程,其具体的实现为:

在待排序的 n 个记录中任选一个进行记录(通常选第一个),作为轴值
进行分区,将所有比轴值小的元素放在轴值左边,所有比轴值大的元素放在轴值的右边,中间为所选的轴值
对左右两个分区递归进行步骤 1、2,递归结束条件是序列的大小是 1

【时空复杂度】
平均时间复杂度为 O(nlogn),平均空间复杂度为 O(logn)

【代码实现】

void quickSort(int a[],int left,int right){
    if(left>=right)
        return;
 
    int pivotKey=a[left];//轴值
    int i=left,j=right;
    while(i<j){
        while(i<j&&a[j]>=pivotKey)//从右向左扫描,找第一个码值小于key的记录,并交换到key
            j--;
        a[i]=a[j];//赋给前面被拿走的a[i]
        while(i<j&&a[i]<=pivotKey)//从左向右扫描,找第一个码值大于key的记录,并交换到右边
            i++;
        a[j]=a[i];//赋给前面被拿走的a[j]
    }
 
    a[i]=pivotKey;//分区元素放到正确位置
    quickSort(a,left,i-1);
    quickSort(a,i+1,right);
}

4.桶排序

【基本思想】
其基本思想是:假设待排序记录的值都在 0~m-1 之间,设置 m 个桶,将值为 i 的记录分配到第 i 个桶中,然后再将各个桶中的距离依次收集起来。

【代码实现】

int bucket[m];//m-1为数据的最大值
void bucketSort(int a[],int n){
    for(int i=0;i<n;i++)//将数组a存入桶中
        bucket[a[i]]++;
    
    int cnt=0;
    for(int i=0;i<m;i++){
        if(bucket[i]!=0){//桶不为空时
            a[cnt++]=i;//收集数据
            bucket[i]--;//桶中数据个数-1
        }
    }
}

二、递归算法

【算法描述】
递归算法就是把一个大型复杂问题分解为小问题,而递归的关键就是找出递归定义和终止条件。
递归算法的解题步骤:
第一,分析问题,寻找大规模问题与小型问题的联系。
第二,控制递归,找出终止条件。
第三,设计函数,实现有关操作。

【经典例题】

1.菲波那契数列

来源:http://ybt.ssoier.cn:8088/problem_show.php?pid=1201

【题目描述】
菲波那契数列是指这样的数列: 数列的第一个和第二个数都为1,接下来每个数都等于前面2个数之和。

给出一个正整数a,要求菲波那契数列中第a个数是多少。

【输入】
第1行是测试数据的组数n,后面跟着n行输入。每组测试数据占1行,包括一个正整数a(1≤a≤20)。

【输出】
输出有n行,每行输出对应一个输入。输出应是一个正整数,为菲波那契数列中第a个数的大小。

【输入样例】
4
5
2
19
1
【输出样例】
5
1
4181
1

【AC代码】

#include
using namespace std;
int a[25];
int main()
{
	int i, j, n;
	int  k;
	a[1] = 1, a[2] = 1;
	for (i = 3; i <=20; i++)
		a[i] = (a[i - 1] + a[i - 2]);
	cin >> n;
	for (j = 0; j < n; j++)
	{
		cin >> k;
		cout << a[k] << endl;
	}return 0;
}

2.Pell数列

来源:http://ybt.ssoier.cn:8088/problem_show.php?pid=1202

【题目描述】
Pell数列a1,a2,a3,…的定义是这样的,a1=1,a2=2,…,an=2an−1+an−2(n>2)。

给出一个正整数 k,要求Pell数列的第 k 项模上 32767 是多少。

【输入】
第1行是测试数据的组数 n,后面跟着 n 行输入。每组测试数据占 1 行,包括一个正整数k(1≤k<1000000)。

【输出】
n 行,每行输出对应一个输入。输出应是一个非负整数。

【输入样例】
2
1
8
【输出样例】
1
408

【AC代码】

#include
using namespace std;
int a[1000005];
int main()
{
	int i, j, n;
	int  k;
	a[1] = 1, a[2] = 2;
	for (i = 3; i <= 1000000; i++)
		a[i] = (2*a[i - 1] + a[i - 2])%32767;
	cin >> n;
	for (j = 0; j < n; j++)
	{
		cin >> k;
		cout << a[k] << endl;
	}
	return 0;
}

3.汉诺塔问题

来源:http://ybt.ssoier.cn:8088/problem_show.php?pid=1205

【题目描述】
约19世纪末,在欧州的商店中出售一种智力玩具,在一块铜板上有三根杆,最左边的杆上自上而下、由小到大顺序串着由64个圆盘构成的塔。目的是将最左边杆上的盘全部移到中间的杆上,条件是一次只能移动一个盘,且不允许大盘放在小盘的上面。

这是一个著名的问题,几乎所有的教材上都有这个问题。由于条件是一次只能移动一个盘,且不允许大盘放在小盘上面,所以64个盘的移动次数是:18,446,744,073,709,551,615

这是一个天文数字,若每一微秒可能计算(并不输出)一次移动,那么也需要几乎一百万年。我们仅能找出问题的解决方法并解决较小N值时的汉诺塔,但很难用计算机解决64层的汉诺塔。

假定圆盘从小到大编号为1, 2, …

【输入】
输入为一个整数(小于20)后面跟三个单字符字符串。

整数为盘子的数目,后三个字符表示三个杆子的编号。

【输出】
输出每一步移动盘子的记录。一次移动一行。

每次移动的记录为例如 a->3->b 的形式,即把编号为3的盘子从a杆移至b杆。

【输入样例】
2 a b c

【输出样例】
a->1->c
a->2->b
c->1->b

【AC代码】

#include
#include
using namespace std;
void Hanoi(int n, char a, char c, char b)
{
    if (n == 1)
    {
        printf("%c->%d->%c\n", a, n, b);
        return;
    }
    Hanoi(n - 1, a, b, c);
    printf("%c->%d->%c\n", a, n, b);
    Hanoi(n - 1, c, a, b);
}
int main()
{
    int n;
    char a, b, c;
    cin >> n >> a >> b >> c;
    Hanoi(n, a, c, b);
    return 0;

}

4.放苹果

来源:http://ybt.ssoier.cn:8088/problem_show.php?pid=1206

【题目描述】
把M个同样的苹果放在N个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法?(用K表示)5,1,1和1,5,1 是同一种分法。

【输入】
第一行是测试数据的数目t(0≤t≤20)。以下每行均包含二个整数M和N,以空格分开。1≤M,N≤10。

【输出】
对输入的每组数据M和N,用一行输出相应的K。

【输入样例】
1
7 3
【输出样例】
8
【AC代码】

#include
#include
using namespace std;
int a[20][20];
int f(int m,int n)
{
    int i,j;
    for(i=1;i<=n;i++)
        a[0][i]=1;
    for(i=1;i<=m;i++)
        a[i][1]=1;
    for(i=1;i<=m;i++)
        for(j=2;j<=n;j++)
            if(i<j)
                a[i][j]=a[i][i];
            else 
                a[i][j]=a[i][j-1]+a[i-j][j]; 
}
int main()
{
    int m,n,i,j,k;
    cin>>k;
    for(i=1;i<=k;i++)
    {
        cin>>m>>n;
        f(m,n);
        cout<<a[m][n]<<endl;
    }
    return 0;
}  

5.求最大公约数问题

来源:http://ybt.ssoier.cn:8088/problem_show.php?pid=1207

【题目描述】
给定两个正整数,求它们的最大公约数。

【输入】
输入一行,包含两个正整数(<1,000,000,000)。

【输出】
输出一个正整数,即这两个正整数的最大公约数。

【输入样例】
6 9
【输出样例】
3

【AC代码】

辗转相除法
#include
using namespace std;
int main()
{
	
	int n, m, t;
	int r=1;
	cin >> n >> m;
	if (n < m)
	{
		t = n;
		n = m;
		m = t;
	}
	while (r)
	{
		r = n % m;
		n = m;
		m = r;
	}
	cout << n;
	return 0;
}

三、递推算法

【概述】
通过已知条件,利用相邻的数据项间的关系(即:递推关系),得出中间推论,直至得到结果的算法。
【递推关系】
给定一个数的序列H0,H1,…,Hn,若存在整数N0,使当n>N0时,可以用=/>/<将Hn与其前面的某些项Hi(0 【特点】
避开了求通项公式的麻烦,把一个复杂问题的求解,分解成了连续的若干简单运算。
【基本思想】
把一个复杂的庞大的计算过程转化为简单过程的多次重复。
逆推与顺推:在计算时,如果可以找到前后过程之间的数量关系(即:递推式),那么,从已知条件推到问题叫顺推,从问题出发推到已知条件叫逆推。
【递推与递归的比较】
相对于递归算法,递推算法免除了数据进出栈的过程,也就是说,不需要函数不断的向边界值靠拢,而直接从边界出发,直到求出函数值。

经典例题
1.菲波那契数列

来源:http://ybt.ssoier.cn:8088/problem_show.php?pid=1188

【题目描述】
菲波那契数列是指这样的数列: 数列的第一个和第二个数都为1,接下来每个数都等于前面2个数之和。

给出一个正整数a,要求菲波那契数列中第a个数对1000取模的结果是多少。

【输入】
第1行是测试数据的组数n,后面跟着n行输入。每组测试数据占1行,包括一个正整数a(1 ≤ a ≤ 1000000)。

【输出】
n行,每行输出对应一个输入。输出应是一个正整数,为菲波那契数列中第a个数对1000取模得到的结果。

【输入样例】
4
5
2
19
1
【输出样例】
5
1
181
1

递推
#include
using namespace std;
int a[1000000];
int main()
{
	int i, j, n;
	int  k;
	a[1] = 1, a[2] = 1;
	for (i = 3; i <= 1000000; i++)
		a[i] = (a[i - 1] + a[i - 2])%1000;
	cin >> n;
	for (j = 0; j < n; j++)
	{
		cin >> k;
		cout << a[k] << endl;
	}
	return 0;
}

2.判断整除

来源:http://ybt.ssoier.cn:8088/problem_show.php?pid=1195

【题目描述】
一个给定的正整数序列,在每个数之前都插入+号或−号后计算它们的和。比如序列:1、2、4共有8种可能的序列:

(+1) + (+2) + (+4) = 7
(+1) + (+2) + (-4) = -1
(+1) + (-2) + (+4) = 3
(+1) + (-2) + (-4) = -5
(-1) + (+2) + (+4) = 5
(-1) + (+2) + (-4) = -3
(-1) + (-2) + (+4) = 1
(-1) + (-2) + (-4) = -7

所有结果中至少有一个可被整数k整除,我们则称此正整数序列可被k整除。例如上述序列可以被3、5、7整除,而不能被2、4、6、8……整除。注意:0、−3、−6、−9……都可以认为是3的倍数。

【输入】
输入的第一行包含两个数:N(2

【输出】
如果此正整数序列可被k整除,则输出YES,否则输出NO。(注意:都是大写字母)

【输入样例】
3 2
1 2 4
【输出样例】
NO

代码

#include
#include
using namespace std;
int a[10100];
int dp[10100][110];
int main()
{
    int n,k,i,j;
    memset(dp,0,sizeof(dp));
    cin>>n>>k;
    for(i=1;i<=n;i++)
        cin>>a[i];
    dp[0][0]=1;
    for(i=1;i<=n;i++)
        for(j=0;j<k;j++)
            dp[i][j]=dp[i-1][(j+a[i])%k]||dp[i-1][(j-a[i]%k+k)%k];
    if(dp[n][0]==0)
        cout<<"NO"<<endl;
    else
        cout<<"YES"<<endl;
    return 0;
}

3.上台阶

来源:http://ybt.ssoier.cn:8088/problem_show.php?pid=1190

时间限制: 1000 ms 内存限制: 65536 KB
提交数: 21120 通过数: 6039
【题目描述】
楼梯有n(71>n>0)阶台阶,上楼时可以一步上1阶,也可以一步上2阶,也可以一步上3阶,编程计算共有多少种不同的走法。

【输入】
输入的每一行包括一组测试数据,即为台阶数n。最后一行为0,表示测试结束。

【输出】
每一行输出对应一行输入的结果,即为走法的数目。

【输入样例】
1
2
3
4
0
【输出样例】
1
2
4
7

AC代码:

#include
using namespace std;
long long a[100] = { 0 };
int main()
{
	
	int i,m;
	a[1] = 1, a[2] = 2,a[3]=4;
	for (i = 4; i <= 100; i++)
	{
		a[i] = a[i - 1] + a[i - 2] + a[i - 3];
	}
	while(cin>>m&&m)
		cout << a[m] << endl;
	return 0;
}

4.吃糖果

来源:http://ybt.ssoier.cn:8088/problem_show.php?pid=1193

【题目描述】
名名的妈妈从外地出差回来,带了一盒好吃又精美的巧克力给名名(盒内共有 N 块巧克力,0

【输入】
输入只有1行,即整数N。

【输出】
输出只有1行,即名名吃巧克力的方案数。

【输入样例】
4
【输出样例】
5

#include
using namespace std;
long long a[30] = { 0 };
int main()
{
	int n, i;
	a[1] = 1; a[2] = 2;
	for (i = 3; i <=30; i++)
		a[i] = a[i - 1] + a[i - 2];
	cin >> n;
		cout << a[n] << endl;

	return 0;
}

四、贪心算法

【算法描述】
贪心算法是一种求最优解的方法,不是从整体上思考问题,而是选择局部最优解,由局部进而得到整个问题的最优解。
利用贪心策略解题,需要注意两个问题:
(1)该题是否适合于用贪心策略求解;
(2)如何选择贪心策略,以得到问题的最优/较优解。
所以如何去选择贪心策略是很重要的。

【经典例题】

1.装箱问题

来源:http://ybt.ssoier.cn:8088/problem_show.php?pid=1226

【题目描述】
一个工厂制造的产品形状都是长方体,它们的高度都是h,长和宽都相等,一共有六个型号,他们的长宽分别为1×1,2×2,3×3,4×4,5×5,6×6。这些产品通常使用一个6×6×h的长方体包裹包装然后邮寄给客户。因为邮费很贵,所以工厂要想方设法的减小每个订单运送时的包裹数量。他们很需要有一个好的程序帮他们解决这个问题从而节省费用。现在这个程序由你来设计。

【输入】
输入包括几行,每一行代表一个订单。每个订单里的一行包括六个整数,中间用空格隔开,分别为1×1至6×6这六种产品的数量。输入将以6个0组成的一行结尾。

【输出】
除了输入的最后一行6个0以外,输入文件里每一行对应着输出文件的一行,每一行输出一个整数代表对应的订单所需的最小包裹数。

【输入样例】
0 0 4 0 0 1
7 5 1 0 0 0
0 0 0 0 0 0
【输出样例】
2
1
代码:

#include
#include
using namespace std;
int main()
{
	int a, b, c, d, e, f;
	while (cin >> a >> b >> c >> d >> e >> f)
	{
		if (a ==0&& b == 0&&c ==0&& d ==0&& e ==0&& f == 0)break;
		int ans = 0;//箱子数
		int v2=0;//剩余2*2的空间
		int v1 = 0;//剩余1*1的空间
		int x[4] = { 0,5,3,1 };//存3*3的剩下的2*2空间
		ans = d + e + f + ceil(c*1.0 / 4);
		v2 = d * 5 + x[c % 4];
		if (b > v2)
			ans += ceil((b - v2)*1.0 / 9);
		v1 = 36 * (ans - f) - 25 * e - 16 * d - 9 * c - 4 * b;
		if (a > v1)
			ans += ceil((a - v1)*1.0 / 36);
		cout << ans << endl;
	}
	return 0;
}

2.寻找平面上的极大点

来源:http://ybt.ssoier.cn:8088/problem_show.php?pid=1230

【题目描述】
在一个平面上,如果有两个点(x,y),(a,b),如果说(x,y)支配了(a,b),这是指x≥a,y≥b;

用图形来看就是(a,b)坐落在以(x,y)为右上角的一个无限的区域内。

给定n个点的集合,一定存在若干个点,它们不会被集合中的任何一点所支配,这些点叫做极大值点。

编程找出所有的极大点,按照x坐标由小到大,输出极大点的坐标。

本题规定:n不超过100,并且不考虑点的坐标为负数的情况。

【输入】
输入包括两行,第一行是正整数n,表示是点数,第二行包含n个点的坐标,坐标值都是整数,坐标范围从0到100,输入数据中不存在坐标相同的点。

【输出】
按x轴坐标最小到大的顺序输出所有极大点。

输出格式为:(x1,y1),(x2,y2),…(xk,yk)。

注意:输出的每个点之间有",“分隔,最后一个点之后没有”,",少输出和多输出都会被判错。

【输入样例】
5
1 2 2 2 3 1 2 3 1 4
【输出样例】
(1,4),(2,3),(3,1)
【提示】
提示:
ACMer必看的基础算法(附经典例题)_第1张图片

#include
#include
using namespace std;
struct node {
	int x, y;
}a[105];
bool cmp(node a, node b)
{
	return a.x < b.x || (a.x == b.x && a.y < b.y);
}
int main()
{
	int n;
	cin >> n;
	
	for (int i = 0; i < n; i++)
		cin >> a[i].x>>a[i].y;
	sort(a, a + n, cmp);
	for (int i = 0; i < n; i++)
	{
		int flag = 1;
		for (int j = i+1; j <= n; j++)
		{
			if (i == n - 1)
			{
				flag = 0;
				cout << '(' << a[i].x << ',' << a[i].y << ')';
				break;
			}
			if (a[i].x <= a[j].x && a[i].y <= a[j].y)
			{
				flag = 0;
				break;
			}
		}

		if (flag)cout << '(' << a[i].x << ',' << a[i].y << ')' << ',';
	}
	return 0;

3.Crossing River

来源:http://ybt.ssoier.cn:8088/problem_show.php?pid=1232

【题目描述】
几个人过河,每次过两人一人回,速度由慢者决定,问过河所需最短时间。

【输入】
输入t组数据,每组数据第1行输入n,第2行输入n个数,表示每个人过河的时间。

【输出】
输出t行数据,每行1个数,表示每组过河最少时间。

【输入样例】
1
4
1 2 5 10
【输出样例】
17

#include
#include
#include
#include
#include
#define INF 999999999
#define N 1001
using namespace std;
int a[N],dp[N];
int main()
{
    int t;
    int n;
    cin>>t;
    while(t--)
    {
        cin>>n;
        for(int i=0;i<n;i++)
            cin>>a[i];
        sort(a,a+n);
        dp[0]=a[0];
        dp[1]=a[1];
        for(int i=2;i<n;i++)
            dp[i]=min(dp[i-1]+a[0]+a[i],dp[i-2]+a[0]+a[i]+a[1]*2);
        cout<<dp[n-1]<<endl;
    }
    return 0;
}

4.最大子矩阵

来源:http://ybt.ssoier.cn:8088/problem_show.php?pid=1224

【题目描述】
已知矩阵的大小定义为矩阵中所有元素的和。给定一个矩阵,你的任务是找到最大的非空(大小至少是1×1)子矩阵。

比如,如下4×4的矩阵

0 9 −4 −1
−2 2 1 8
−7 −6 −4 0
0 2 1 −2
的最大子矩阵是

9 −4
−1 2
1 8
这个子矩阵的大小是15。

【输入】
输入是一个N×N的矩阵。输入的第一行给出N(0

【输出】
输出最大子矩阵的大小。

【输入样例】
4
0 -2 -7 0
9 2 -6 2
-4 1 -4 1
-1 8 0 -2
【输出样例】
15

#include
#include
#include
#include
#include
#define INF 999999999
#define N 1001
using namespace std;
int a[N][N],b[N][N];
int main()
{
    int n;
    int x,y;
    int num,sum;
    cin>>n;
    for(int i=1;i<=n*n;i++)
    {
        cin>>num;
        x=(i-1)/n+1;
        y=i%n;
        if(y==0)
            y=n;
        a[x][y]=num;
    }
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            b[i][j]=b[i-1][j]+a[i][j];
    int max=b[1][1];
    for(int i=1;i<=n;i++)
        for(int j=i;j<=n;j++)
        {
            sum=0;
            for(int k=1;k<=n;k++)
            {
                if(sum>0)
                    sum+=b[j][k]-b[i-1][k];
                else
                    sum=b[j][k]-b[i-1][k];
                if(sum>max)
                    max=sum;
            }
        }
    cout<<max<<endl;
    return 0;
}

5.电池的寿命

来源:http://ybt.ssoier.cn:8088/problem_show.php?pid=1229

【题目描述】
小S新买了一个掌上游戏机,这个游戏机由两节5号电池供电。为了保证能够长时间玩游戏,他买了很多5号电池,这些电池的生产商不同,质量也有差异,因而使用寿命也有所不同,有的能使用5个小时,有的可能就只能使用3个小时。显然如果他只有两个电池一个能用5小时一个能用3小时,那么他只能玩3个小时的游戏,有一个电池剩下的电量无法使用,但是如果他有更多的电池,就可以更加充分地利用它们,比如他有三个电池分别能用3、3、5小时,他可以先使用两节能用3个小时的电池,使用半个小时后再把其中一个换成能使用5个小时的电池,两个半小时后再把剩下的一节电池换成刚才换下的电池(那个电池还能用2.5个小时),这样总共就可以使用5.5个小时,没有一点浪费。

现在已知电池的数量和电池能够使用的时间,请你找一种方案使得使用时间尽可能的长。

【输入】
输入包含多组数据。每组数据包括两行,第一行是一个整数N(2≤N≤1000),表示电池的数目,接下来一行是N个正整数表示电池能使用的时间。

【输出】
对每组数据输出一行,表示电池能使用的时间,保留到小数点后1位。

【输入样例】
2
3 5
3
3 3 5
【输出样例】
3.0
5.5

#include
#include
using namespace std;
int main()
{
	int n;
	int t;
	int sum, max = 0;
	while (cin >> n)
	{
		sum = 0;
		for (int i = 0; i < n; i++)
		{
			cin >> t;
			sum += t;
			if (t > max)max = t;
		}
		if (max >= sum - max)
			cout << fixed << setprecision(1) << (sum - max) * 1.0 << endl;
		else
			cout << fixed << setprecision(1) << sum * 1.0 / 2 << endl;
	}
	return 0;
}

五、动态规划

基本模型

动态规划算法通常用于求解具有某种最优性质的问题,在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填人表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。

基本概念
阶段:把问题分成几个相互联系的有顺序的几个环节,这些环节即称为阶段。

状态:某一阶段的出发位置称为状态。通常一个阶段包含若干状态。如图7-1中,阶段3就有3个状态结点4、5、6

决策:从某阶段的一个状态演变到下一个阶段某状态的选择。

策略:由开始到终点的全过程中,由每段决策组成的决策序列称为全过程策略,简称策略。

状态转移方程:前一阶段的终点就是后一阶段的起点,前一阶段的决策选择导出了后阶段的状态,这种关系描述了由k阶段到k+1阶段状态的演变规律,称为状态转移方程。

基本原理
1.最优化原理

最优化原理可这样阐述:一个最优化策略具有这样的性质,不论过去状态和决策如何对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。
最优化原理是动态规划的基础,任何问题,如果失去了最优化原理的支持,就不可能用动态规划方法计算。
2.无后效性
过去的步骤只能通过当前状态影响未来的发展,当前的状态是历史的总结”。这条特征说明动态规划只适用于解决当前决策与过去状态无关的问题。状态,出现在策略任何个位置,它的地位相同,都可实施同样策略,这就是无后效性的内涵。
由上可知,最优化原理,无后效性,是动态规划必须符合的两个条件。

基本步骤
设计一个标准的动态规划算法,通常可按以下几个步骤进行

(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。注意这若干个阶段一定要是有序的或者是可排序的(即无后向性),否则问题就无法用动态规划求解。

(2)选择状态:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。

(3)确定决策并写出状态转移方程:之所以把这两步放在一起,是因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以,如果我们确定了决策,状态转移方程也就写出来了。但事实上,我们常常是反过来做的,根据相邻两段的各状态之间的关系来确定决策。

(4)写出规划方程(包括边界条件):动态规划的基本方程是规划方程的通用形式化表达式般说来,只要阶段、状态、决策和状态转移确定了,这一步还是比较简单的。

当然,在此之前需要先进行判断题目是否可以使用动态规划解决,也就是说是否满足两个基本定理。

经典例题
1.数字三角形问题
题意
给定一个由n行数字组成的数字三角形\。试设计一个算法,计算出从三角形的顶至底的一条路径,使该路径经过的数字总和最大。
对于给定的由n行数字组成的数字三角形,计算从三角形的顶至底的路径经过的数字和的最大值。

Input
输入数据的第1行是数字三角形的行数n,1≤n≤100。接下来n行是数字三角形各行中的数字。所有数字在0…99之间。

Output
输出数据只有一个整数,表示计算出的最大值.

样例输入
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
样例输出
30

思路实现:
刚看到这个题,第一反应是递归…,要不是dp的例题我就用递归来做了,不过很明显递归肯定是会超时的。就往dp的思路上靠,感觉这个题是入门级题目,思路比较简单,从最下面一行开始逐一往上进行计算

代码:

#include
#include
using namespace std;
#define MAX 101;
int dp[101][101];
int n;
int Sum[101][101];
int main() 
{
    int i, j;
    cin >> n;
    for (i = 1; i <= n; i++)
        for (j = 1; j <= i; j++)
            cin >> dp[i][j];
    for (int i = 1; i <= n; i++)
        Sum[n][i] = dp[n][i];
    for (int i = n - 1; i >= 1; i--)
        for (int j = 1; j <= i; j++)
            Sum[i][j] =
            max(Sum[i + 1][j], Sum[i + 1][j + 1]) + dp[i][j];
            cout << Sum[1][1] << endl;
}

2.最长公共子序列
题意
给出两个字符串,求出这样的一个最长的公共子序列的长度:子序列中的每个字符都能在两个原串中找到,而且每个字符的先后顺序和原串中的先后顺序一致。

Sample Input

abcfbc abfcab
programming contest
abcd mnp

Sample Output

4
2
0

思路实现:
这题老师上课也讲了,但我感觉理解的不到位,自己再总结一下

①确定状态:
设F[x][y]表示S[1…x]与T[1…y]的最长公共子序列的长度

②确定状态转移方程和边界条件:
D分三种情况来考虑:
S[x]不在公共子序列中:该情况下FxLy]=F[x-1][y]
T[y]不在公共子序列中:该情况下F[x][y]=F[x-1][y-1]
S[x]=Ty],S[x]与T[y]在公共子序列中:该情况下,F[x][y]=F[x-1][y-1]+1
F[x][y]取上述三种情况的最大值。

综上:
状态转移方程:Fx[y]=max{F[x-1ILy], FLXILY-1],FLx-11[y-11+1)其中第三种情况要满足SLx]=T[y];
边界条件:F[0][y]=0,F[x][0]=0

③程序实现:
计算F[x][y]时用到F[x-1][y-1],F[x-1][y],F[x][y-1]这些状态,它们要么在F[x][y]的上一行,要么在F[x][y]的左边。因此预处理出第0行,然后按照行从小到大、同一行按照列从小到大的顺序来计算就可以用选代法计算出来。
代码:

#include
#include
#include
using namespace std;
const int MAXN = 5005;
string S, T;
int F[MAXN][MAXN];
int main()
{
	cin >> S;
	cin >> T;
	int ls = S.length(), lt = T.length();
	for (int i = 1; i <= ls; i++)
		for (int j = 1; j <= lt; j++)
		{
			F[i][j] = max(F[i - 1][j], F[i][j - 1]);
			if (S[i - 1] == T[j - 1])
				F[i][j] = max(F[i][j], F[i - 1][j - 1] + 1);
		}

	cout << F[ls][lt] << endl;
	return 0;
}

3.最大上升子序列
题意
一个数的序列ai,当a1 < a2 < … < aS的时候,我们称这个序
列是上升的。对于给定的一个序列(a1, a2, …, aN),我们可以得到一些上升的子序列(ai1, ai2, …, aiK),这里1 <= i1 < i2 < … < iK<= N。比如,对于序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。这些子序列中最长的长度是4,比如子序列(1, 3, 5, 8).你的任务,就是对于给定的序列,求出最长上升子序列的长度。

输入
输入的第一行是序列的长度N (1 <= N <= 1000)。第二行给
出序列中的N个整数,这些整数的取值范围都在0到10000。

输出
最长上升子序列的长度。

输入样例
7
1 7 3 5 9 4 8

输出样例
4

思路实现:
这也是典型的例题,自己总结一下子,加深下理解。

1.找子问题
“求序列的前n个元素的最长上升子序列的长度”是个子问题,但这样分解子问题,不具有“无后效性”假设F(n) = x,但可能有多个序列满足F(n) = x。有的序列的最后一个元素比 an+1小,则加上an+1就能形成更长上升子序列;有的序列最后一个元素不比an+1小……以后的事情受如何达到状态n的影响,不符合“无后效性”“求以ak(k=1, 2, 3…N)为终点的最长上升子序列的长度,一个上升子序列中最右边的那个数,称为该子序列的“终点”。
虽然这个子问题和原问题形式上并不完全一样,但是只要这N个子问题都解决了,那么这N个子问题的解中,最大的那个就是整个问题的解。

确定状态:
子问题只和一个变量-- 数字的位置相关。因此序列中数的位置k 就是“状态”,而状态 k 对应的“值”,就是以ak做为“终点”的最长上升子序列的长度。状态一共有N个。

找出状态转移方程:
maxLen (k)表示以ak做为“终点”的
最长上升子序列的长度那么:
初始状态:maxLen (1) = 1
maxLen (k) = max { maxLen (i):1<=i < k 且 ai < ak且 k≠1 } + 1
若找不到这样的i,则maxLen(k) = 1
maxLen(k)的值,就是在ak左边,“终点”数值小于ak ,且长度最大的那个上升子序列的长度再加1。因为ak左边任何“终点”小于ak的子序列,加上ak后就能形成一个更长的上升子序列。

代码

#include 
#include 
#include 
using namespace std;
const int MAXN =1010;
int a[MAXN]; int maxLen[MAXN];
int main() 
{
	int N; cin >> N;
	for( int i = 1;i <= N;++i) 
	{
		cin >> a[i]; 
		maxLen[i] = 1;
	}
	for( int i = 2; i <= N; ++i) 
	{ 
		//每次求以第i个数为终点的最长上升子序列的长度
		for( int j = 1; j < i; ++j) 
		//察看以第j个数为终点的最长上升子序列
		if( a[i] > a[j] )
			maxLen[i] = max(maxLen[i],maxLen[j]+1); 
	}
	cout << * max_element(maxLen+1,maxLen + N + 1 );
	return 0;
} 

4.最大子矩阵和
题意
一个MN的矩阵,找到此矩阵的一个子矩阵,并且这个子矩阵的元素的和是最大的,输出这个最大的值。
例如:33的矩阵:
-1 3 -1
2 -1 3
-3 1 2
和最大的子矩阵是:
3 -1
-1 3
1 2

Input
第1行:M和N,中间用空格隔开(2 <= M,N <= 500)。
第2 - N + 1行:矩阵中的元素,每行M个数,中间用空格隔开。(-10^9 <= M[i] <= 10^9)

Output
输出和的最大值。如果所有数都是负数,就输出0。

思路
也是经典例子,解法与动态规划经典题目——最大连续子序列之和题目思想一样,只不过是二维空间上的拓展。N*N矩阵用二维数组a[N][N]表示。通过思考可以发现,这道题目与动态规划经典题目——最大连续子序列之和非常相似,只不过动态规划经典题目——最大连续子序列之和它是一维的问题,我们可以把每列的元素进行合并为一个元素(可以定义二维数组sum[][],其中sum[i]表示前i行每列数字相加的和,所以sum[j][] - sum[i][]为一维向量),这样运用动态规划经典题目——最大连续子序列之和的思想了。

代码:

#include
#include
#include
#include
#include
using namespace std;
typedef long long ll;
const int N = 1000;
ll arr[N][N];
ll dp[N][N];
int m, n;
int main()
{
	while (cin>>m>>n)
	{
		memset(dp, 0, sizeof(dp));
		for (int i = 1; i <= n; i++)
		{
			for (int j = 1; j <= m; j++)
			{
				cin >> arr[i][j];
				dp[i][j] = dp[i - 1][j] + arr[i][j];
			}
		}
		ll ans = arr[1][1];
		for (int i = 1; i <= n; i++)
			for (int j = i; j <= n; j++) 
			{
				ll sum = 0;
				for (int k = 1; k <= m; k++)
				{
					sum += dp[j][k] - dp[i - 1][k]; 
					if (sum < 0)
						sum = 0;
					if (ans < sum)
						ans = sum;
				}
			}
		cout << ans << endl;
	}
	return 0;
}

区间dp

【概述】
区间型动态规划,又称为合并类动态规划,是线性动态规划的扩展,它在分阶段地划分问题时,与阶段中元素出现的顺序和由前一阶段的区间中哪些元素合并而来有很大的关系。

【思想】
区间 DP 实质上就是在一个区间上进行的动态规划,不仅要满足 DP 问题的最优子结构,还要符合在区间上操作的特点。

其主要思想就是在小区间进行 DP 得到最优解,然后再利用小区间的最优解合并求大区间的最优解。

往往会枚举区间,将区间分成左右两部分,再求出左右区间进行合并操作。这样一来,如果要得知一个大区间的情况,由于它必定是由从多个长度不一的小区间转移而来,那么可以通过求得多个小区间的情况,从而合并信息,得到大区间。

即:已知区间长度为 len-1 的所有状态,然后可以通过小于 len 的状态转移到区间长度为 len 的状态,一般是在外层循环遍历 len,内层循环遍历起点来解决。

for k:=1 to n-1 do //区间长度
for i:=1 to n-k do //区间起点
for j:=i to i+k-1 do //区间中任意点
dp[i,i+k]:=max{dp[i,j] + dp[j+1,i+k] + a[i,j] + a[j+1,i+k]};
状态转移方程:寻找区间 dp[i,i+k] 的一种合并方式 dp[i,j] + dp[j+1,i+k],使得其值最大或最小

区间长度 k 必须要放到第一层循环,来保证方程中状态 dp[i,j]、dp[j+1,i+k] 值在 dp[i,i+k] 之前就已计算出来

其中 a[i,j]+a[j+1,i+k] 可以灵活多变,其指的是合并区间时产生的附加值

经典题目
1.
Gappu has a very busy weekend ahead of him. Because, next weekend is Halloween, and he is planning to attend as many parties as he can. Since it’s Halloween, these parties are all costume parties, Gappu always selects his costumes in such a way that it blends with his friends, that is, when he is attending the party, arranged by his comic-book-fan friends, he will go with the costume of Superman, but when the party is arranged contest-buddies, he would go with the costume of ‘Chinese Postman’.

Since he is going to attend a number of parties on the Halloween night, and wear costumes accordingly, he will be changing his costumes a number of times. So, to make things a little easier, he may put on costumes one over another (that is he may wear the uniform for the postman, over the superman costume). Before each party he can take off some of the costumes, or wear a new one. That is, if he is wearing the Postman uniform over the Superman costume, and wants to go to a party in Superman costume, he can take off the Postman uniform, or he can wear a new Superman uniform. But, keep in mind that, Gappu doesn’t like to wear dresses without cleaning them first, so, after taking off the Postman uniform, he cannot use that again in the Halloween night, if he needs the Postman costume again, he will have to use a new one. He can take off any number of costumes, and if he takes off k of the costumes, that will be the last k ones (e.g. if he wears costume A before costume B, to take off A, first he has to remove B).

Given the parties and the costumes, find the minimum number of costumes Gappu will need in the Halloween night.

Input
Input starts with an integer T (≤ 200), denoting the number of test cases.

Each case starts with a line containing an integer N (1 ≤ N ≤ 100) denoting the number of parties. Next line contains N integers, where the ith integer ci (1 ≤ ci ≤ 100) denotes the costume he will be wearing in party i. He will attend party 1 first, then party 2, and so on.

Output
For each case, print the case number and the minimum number of required costumes.

Sample Input
2
4
1 2 1 2
7
1 2 1 1 3 2 1

Sample Output
Case 1: 3
Case 2: 4

题意:
给你n天需要穿的衣服的样式,每次可以套着穿衣服,脱掉的衣服就不能再用了(可以再穿),问至少要带多少条衣服才能参加所有宴会

思路:区间DP
dp[i][j]代表从区间i到区间j需要的最少穿衣服数量。
考虑第i天,如果后面的[i+1, j]天的衣服没有和第i天相同的,那么dp[i][j] = dp[i + 1][j] + 1。
然后在区间[i +1, j]里面找到和第i天衣服一样的日子,尝试直到那天都不把i脱掉,也就是说如果这件衣服不脱 掉的话,
那么就会有dp[i][j] = dp[i + 1][k - 1] + dp[k][j],取所有可能情况的一个最小值就可以。

代码

#include 
#include 
#include 
#include

using namespace std;
typedef long long LL;
const int N = 1e2 + 10;
int n, m;
int num[N];
int dp[N][N];

int main()
{
    int t;
    cin >> t;
    int Case = 0;
    while (t--)
    {
        cin >> n;
        for (int i = 1; i <= n; ++i)
            cin>>num[i];
        for (int i = n; i >= 1; --i)
            for (int j = i; j <= n; ++j)
            {
                dp[i][j] = dp[i + 1][j] + 1;
                for (int k = i + 1; k <= j; ++k)
                {
                    if (num[k] == num[i])dp[i][j] = min(dp[i][j], dp[i + 1][k - 1] + dp[k][j]);
                }
            }
        printf("Case %d: %d\n", Case++, dp[1][n]);
    }
}

We give the following inductive definition of a “regular brackets” sequence:

the empty sequence is a regular brackets sequence,
if s is a regular brackets sequence, then (s) and [s] are regular brackets sequences, and
if a and b are regular brackets sequences, then ab is a regular brackets sequence.
no other sequence is a regular brackets sequence
For instance, all of the following character sequences are regular brackets sequences:

(), [], (()), ()[], ()[()]

while the following character sequences are not:

(, ], )(, ([)], ([(]

Given a brackets sequence of characters a1a2 … an, your goal is to find the length of the longest regular brackets sequence that is a subsequence of s. That is, you wish to find the largest m such that for indices i1, i2, …, im where 1 ≤ i1 < i2 < … < im ≤ n, ai1ai2 … aim is a regular brackets sequence.

Given the initial sequence ([([]])], the longest regular brackets subsequence is [([])].

Input
The input test file will contain multiple test cases. Each input test case consists of a single line containing only the characters (, ), [, and ]; each input test will have length between 1 and 100, inclusive. The end-of-file is marked by a line containing the word “end” and should not be processed.

Output
For each input case, the program should print the length of the longest possible regular brackets subsequence on a single line.

Sample Input
((()))
()()()
([]])
)[)(
([][][)
end

Sample Output
6
6
4
0
6

题意:
给出一个 只有()[]的 字符串,问最多有多少括号能匹配(一对括号算2)

思路:区间DP
看起来很像经典的栈应用,但是那个是判断是否完全匹配的

dp[i][j]为区间[i,j]的最大完全匹配数
那么对于dp[i][j]
1.如果边界两个匹配,显然有 dp[i][j]=dp[i+1][j-1]+2;
2.如果边界不匹配,那么dp[i][j] = max(dp[i][j] , dp[i][k] + dp[k+1][j])。

代码

#include
#include
#include
#include
using namespace std;
const int N = 100 + 10;
int dp[N][N];
char str[N];
int main() 
{
	while (cin>>str + 1&&str[1] != 'e') 
	{
		memset(dp, 0, sizeof(dp));
		int n = strlen(str + 1);
		for (int len = 2; len <= n; len++) 
		{
			for (int i = 1; i <= n; i++) 
			{
				int j = i + len - 1;
				if (j > n) 
					continue;
				if (str[i] == '(' && str[j] == ')' || str[i] == '[' && str[j] == ']') 
					dp[i][j] = dp[i + 1][j - 1] + 2;
				for (int k = i; k < j; k++) 
					dp[i][j] = max(dp[i][j], dp[i][k] + dp[k + 1][j]);
			}
		}
		printf("%d\n", dp[1][n]);
	}
	return 0;
}

背包问题

背包问题(Knapsack problem)是一种组合优化的NP完全问题。

问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中。

也可以将背包问题描述为决定性问题,即:在总重量不超过W的前提下,总价值是否能达到V?

01背包

【概述】
0-1背包问题是最基本的背包问题,它包含了背包问题中设计状态、方程的最基本思想,另外,别的类型的背包问题往往也可以转换成0-1背包问题求解。

特点:每种物品仅有一件,可以选择放或不放。

【模板】

#include
#define MAX 101
using namespace std;
int V,N;
int w[MAX],c[MAX],f[MAX];
void ZeroOnePack(int cost,int weight)
{
    for(int v=V;v>=weight;v--)
        f[v]=max(f[v],f[v-weight]+cost);
}
 
int main()
{
    cin>>V>>N;
    for(int i=1;i<=N;i++)
        cin>>w[i]>>c[i];
    for(int i=1;i<=N;i++)
        ZeroOnePack(c[i],w[i]);
    cout<<f[V]<<endl;
    return 0;
}

例题

【题目】
有N件物品和一个容量为V的背包。第i件物品的体积是w[i],价值是c[i]。求解将哪些物品装入背包可使价值总和最大。

【基本思路】
子问题:将前i件物品放入容量为v的背包中。

若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。

如果不放第i件物品,那么子问题就转化为“前i-1件物品放入容量为v的背包中”,价值为:f[i-1][v];
如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-w[i]的背包中”,此时能获得的最大价值就是f[i-1][v-w[i]]再加上通过放入第i件物品获得的价值w[i],即:f[i-1][v-w[i]]+c[i]。
用子问题定义状态:f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。

则其状态转移方程:f[i][v]=max{ f[i-1][v] , f[i-1][v-w[i]]+c[i] }

【初始化的细节问题】
我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。

有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满,而是只希望价格尽量大。一种区别这两种问法的实现方法是在初始化的时候有所不同。

如果要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1…V]均设为-∞
如果要求价格尽量最大,初始化时应该将f[0…V]全部设为0
原因:初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。

如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0的nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞了。
如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。
注:此种方法可推广至各种背包问题,后续不再讲解

【优化空间复杂度】
以上方法的时间和空间复杂度均为O(N*V),其中时间复杂度基本已经不能再优化了,但空间复杂度却可以优化到O(V)。

先考虑上面讲的基本思路如何实现,肯定是有一个主循环i=1…N,每次算出来二维数组f[i][0…V]的所有值。

那么,如果只用一个数组f[0…V],能不能保证第i次循环结束后f[v]中表示的就是我们定义的状态f[i][v]呢?f[i][v]是由f[i-1][v]和f[i-1][v-w[i]]两个子问题递推而来,能否保证在推f[i][v]时(也即在第i次主循环中推f[v]时)能够得到f[i-1][v]和f[i-1][v-w[i]]的值呢?

事实上,这要求在每次主循环中我们以v=V…0的顺序推f[v],这样才能保证推f[v]时f[v-w[i]]保存的是状态f[i-1][v-w[i]]的值。

伪代码

for i=1..N
    for v=V..0
        f[v]=max{f[v],f[v-w[i]]+c[i]};

其中,f[v]=max{f[v],f[v-w[i]]}相当于转移方程f[i][v]=max{f[i-1][v],f[i-1][v-w[i]]},因为现在的f[v-w[i]]就相当于原来的f[i-1][v-w[i]]。如果将v的循环顺序从上面的逆序改成顺序的话,那么则成了f[i][v]由f[i][v-w[i]]推知,与本题意不符,但它却是另一个重要的背包问题P02最简捷的解决方案,故学习只用一维数组解01背包问题是十分必要的。

事实上,使用一维数组解01背包的程序在后面会被多次用到,所以这里抽象出一个处理一件01背包中的物品过程,以后的代码中直接调用不加说明。

伪代码

/*
    过程ZeroOnePack,表示处理一件01背包中的物品
    两个参数cost、weight分别表明这件物品的价值和体积
*/
procedure ZeroOnePack(cost,weight)
    for v=V..weight
        f[v]=max{f[v],f[v-weight]+cost}

注意这个过程里的处理与前面给出的伪代码有所不同。前面的示例程序写成v=V…0是为了在程序中体现每个状态都按照方程求解了,避免不必要的思维复杂度。而这里既然已经抽象成看作黑箱的过程了,就可以加入优化。费用为cost的物品不会影响状态f[0…cost-1],这是显然的。

有了这个过程以后,01背包问题的伪代码就可以这样写:

for i=1..N
    ZeroOnePack(c[i],w[i]);

经典题目

1.装箱问题
【题目描述】
有一个箱子容量为V(正整数,0≤v≤20000),同时有n个物品(0< n ≤30),每个物品有一个体积(正整数)。

要求n个物品中,任取若干个装入箱内,使箱子的剩余空间为最小。

【输入】
第一行是一个整数V,表示箱子容量。

第二行是一个整数n,表示物品数。

接下来n行,每行一个正整数(不超过10000),分别表示这n个物品的各自体积。

【输出】
一个整数,表示箱子剩余空间。

【输入样例】
24
6
8
3
12
7
9
7

【输出样例】
0

#include
#include
#include
#include
#include
#include
#include
#include
#include
#define INF 0x3f3f3f3f
#define PI acos(-1.0)
#define N 50
#define MOD 2520
#define E 1e-12
using namespace std;
int n,V;
int w[50],f[20000];
void ZeroOnePack(int weight)
{
    for(int v=V;v>=weight;v--)
        f[v]=max(f[v],f[v-weight]+weight);
}
int main()
{
    cin>>V>>n;
    for(int i=1;i<=n;i++)
        cin>>w[i];
    for(int i=1;i<=n;i++)
        ZeroOnePack(w[i]);
    cout<<V-f[V]<<endl;
    return 0;
}

2.采药
【题目描述】
辰辰是个很有潜能、天资聪颖的孩子,他的梦想是称为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”

如果你是辰辰,你能完成这个任务吗?

【输入】
输入的第一行有两个整数T(1≤T≤1000)和M(1≤M≤100),T代表总共能够用来采药的时间,M代表山洞里的草药的数目。接下来的M行每行包括两个在1到100之间(包括1和100)的的整数,分别表示采摘某株草药的时间和这株草药的价值。

【输出】
输出只包括一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。

【输入样例】
70 3
71 100
69 1
1 2
【输出样例】
3

#include
#include
#include
#include
#include
#include
#define INF 0x3f3f3f3f
#define N 10001
using namespace std;
int t,m;
int w[N],c[N],f[N];
void ZeroOnePack(int cost,int weight)
{
    for(int v=t;v>=weight;v--)
        f[v]=max(f[v],f[v-weight]+cost);
}
int main()
{
    cin>>t>>m;
    for(int i=1;i<=m;i++)
        cin>>w[i]>>c[i];
    for(int i=1;i<=m;i++)
        ZeroOnePack(c[i],w[i]);
    cout<<f[t]<<endl;
    return 0;
}

完全背包

【概述】
完全背包问题也是一个相当基础的背包问题,它有两个状态转移方程,分别在“基本思路”以及“O(VN)的算法“的小节中给出。

【模板】

#include
#define MAX 101
using namespace std;
int V,N;
int w[MAX],c[MAX],f[MAX];
void CompletePack(int cost,int weight)
{
    for(int v=weight;v<=V;v++)
        f[v]=max(f[v],f[v-weight]+cost);
}
 
int main()
{
    cin>>V>>N;
    for(int i=1;i<=N;i++)
        cin>>w[i]>>c[i];
    for(int i=1;i<=N;i++)
        CompletePack(c[i],w[i]);
    cout<<f[V]<<endl;
    return 0;
}

例题

【题目】
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的体积是w[i],价值是c[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

【基本思路】
这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很多种。

如果仍然按照解01背包时的思路,令f[i][v]表示前i种物品恰放入一个容量为v的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:f[i][v]=max{ f[i-1][v-kw[i]] + kc[i] } (0<=k*w[i]<=v)

这跟01背包问题一样有O(N*V)个状态需要求解,但求解每个状态的时间已经不是常数了,求解状态f[i][v]的时间是O(v/w[i]),总的复杂度是超过O(VN)的。

将01背包问题的基本思路加以改进,得到了这样一个清晰的方法。这说明01背包问题的方程的确是很重要,可以推及其它类型的背包问题。但我们还是试图改进这个复杂度。

【简单有效的优化】
若两件物品i、j满足w[i]<=w[j]且c[i]>=c[j],则将物品j去掉,不用考虑。

原因:任何情况下都可将价值小费用高得j换成物美价廉的i,得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。

这个优化可以简单的O(N^2)地实现,一般都可以承受。

另外,针对背包问题而言,比较不错的一种方法是:首先将费用大于V的物品去掉,然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可以O(V+N)地完成这个优化。

【转化为01背包问题求解】
既然01背包问题是最基本的背包问题,那么我们可以考虑把完全背包问题转化为01背包问题来解。

最简单的想法是,考虑到第i种物品最多选V/w[i]件,于是可以把第i种物品转化为V/w[i]件费用及价值均不变的物品,然后求解这个01背包问题。

虽然这样没有改进基本思路的时间复杂度,但这给了我们将完全背包问题转化为01背包问题的思路:将一种物品拆成多件物品。

更高效的转化方法是:将第i种物品拆成费用为w[i]*2k、价值为c[i]*2k的若干件物品,其中k满足w[i]*2^k<=V

原因:这是二进制的思想,不管最优策略选几件第i种物品,总可以表示成若干个2^k件物品的和。这样把每种物品拆成O(log(V/w[i]))件物品,是一个很大的改进。

【O(VN)的算法】
这个算法使用一维数组

伪代码

for i=1..N
    for v=0..V
        f[v]=max{f[v],f[v-weight]+cost}

你会发现,这个伪代码与P01的伪代码只有v的循环次序不同而已。为什么这样一改就可行呢?

首先想想为什么P01中要按照v=V…0的逆序来循环。这是因为要保证第i次循环中的状态f[i][v]是由状态f[i-1][v-w[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果f[i-1][v-w[i]]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[i][v-w[i]],所以就可以并且必须采用v=0…V的顺序循环。这就是这个简单的程序为何成立的道理。

这个算法也可以以另外的思路得出。

例如,基本思路中的状态转移方程可以等价地变形成这种形式:f[i][v]=max{ f[i-1][v] , f[i][v-w[i]]+c[i] }

将这个方程用一维数组实现,便得到了上面的伪代码。

最后抽象出处理一件完全背包类物品的过程

伪代码

procedure CompletePack(cost,weight)
    for v=weight..V
        f[v]=max{f[v],f[v-weight]+cost

经典题目

1.完全背包问题

时间限制: 1000 ms 内存限制: 65536 KB
提交数: 10671 通过数: 5742

【题目描述】
设有n种物品,每种物品有一个重量及一个价值。但每种物品的数量是无限的,同时有一个背包,最大载重量为M,今从n种物品中选取若干件(同一种物品可以多次选取),使其重量的和小于等于M,而价值的和为最大。

【输入】
第一行:两个整数,M(背包容量,M≤200)和N(物品数量,N≤30);

第2…N+1行:每行二个整数Wi,Ci,表示每个物品的重量和价值。

【输出】
仅一行,一个数,表示最大总价值。

【输入样例】
10 4
2 1
3 3
4 5
7 9
【输出样例】
max=12

AC代码:

#include
#include
#include
#include
#include
#include
#include
#include
#include
#define INF 0x3f3f3f3f
#define PI acos(-1.0)
#define N 1001
#define MOD 2520
#define E 1e-12
using namespace std;
int m,n;
int w[N],c[N],f[N];
void CompletePack(int cost,int weight)
{
    for(int v=weight;v<=m;v++)
        f[v]=max(f[v],f[v-weight]+cost);
}
int main()
{
    cin>>m>>n;
    for(int i=1;i<=n;i++)
        cin>>w[i]>>c[i];
    for(int i=1;i<=n;i++)
        CompletePack(c[i],w[i]);
    cout<<"max="<<f[m]<<endl;
    return 0;
}

多重背包

【概述】
每种物品有一个固定的次数上限。

【模板】

#include
#define MAX 101
using namespace std;
int V,N;
int w[MAX],c[MAX],num[MAX],f[MAX];
void MultiplePack(int cost,int weight,int num)
{
    for(int j=V;j>=0;j--)
        for(int k=0;k<=num;k++)
            if(j-k*weight>=0)
                f[j]=max(f[j],f[j-k*weight]+k*cost);
}
int main()
{
    cin>>N>>V;
    for(int i=1;i<=N;i++)
        cin>>w[i]>>c[i]>>num[i];
    for(int i=1;i<=N;i++)
        MultiplePack(c[i],w[i],num[i]);
    cout<<f[V]<<endl;
    return 0;
}

【题目】
有 N 种物品和一个容量为 V 的背包。第 i 种物品最多有 num[i] 件可用,每件体积是 w[i],价值是 c[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

【基本思路】
和完全背包问题很类似,基本的方程只需将完全背包问题的方程略微一改即可。

对于第 i 种物品有 num[i]+1种策略:取 0 件,取 1 件……取 num[i] 件,令 f[i][v] 表示前 i 种物品恰放入一个容量为 V 的背包的最大权值。

其与完全背包的区别在于,完全背包中的物品是不限量的,而多重背包的第 i 种物品最多取 num[i] 个

则有状态转移方程:f[i][v]=max{ f[i-1][v-kw[i]]+kc[i] }(0<=k<=num[i]),时间复杂度是O(V*Σn[i])

考虑对 f[i][j] 进行空间优化,与完全背包

【转化为01背包问题求解】
转化为01背包求解:把第i种物品换成n[i]件01背包中的物品,则得到了物品数为Σn[i]的0-1背包问题,直接求解,复杂度仍然是O(V*Σn[i])。

我们期望将它转化为01背包问题之后能够像完全背包一样降低复杂度。仍然考虑二进制的思想,我们考虑把第i种物品换成若干件物品,使得原问题中第i种物品可取的每种策略——取0…n[i]件——均能等价于取若干件代换以后的物品。另外,取超过n[i]件的策略必不能出现。

方法:将第i种物品分成若干件物品,其中每件物品有一个系数,这件物品的体积和价值均是原来的体积和价值乘以这个系数。使这些系数分别为:1,2,4,…,2(k-1),n[i]-2k+1,且k是满足n[i]-2^k+1>0的最大整数。

例如,如果n[i]为13,就将这种物品分成系数分别为1,2,4,6的四件物品。

分成的这几件物品的系数和为n[i],表明不可能取多于n[i]件的第i种物品。另外这种方法也能保证对于0…n[i]间的每一个整数,均可以用若干个系数的和表示,这个证明可以分0…2k-1和2k…n[i]两段来分别讨论得出。

这样就将第i种物品分成了O(log n[i])种物品,将原问题转化为了复杂度为O(V*Σlog n[i])的01背包问题,是很大的改进。

下面给出O(log amount)时间处理一件多重背包中物品的伪代码:

/*amount表示物品的数量*/
procedure MultiplePack(cost,weight,amount)
    if weight*amount>=V
        CompletePack(cost,weight)
        return
    integer k=1
    while k<num
        ZeroOnePack(k*cost,k*weight)
        amount=amount-k
        k=k*2
    ZeroOnePack(amount*cost,amount*weight)

经典题目

1.庆功会

时间限制: 1000 ms 内存限制: 65536 KB
提交数: 8436 通过数: 4858
【题目描述】
为了庆贺班级在校运动会上取得全校第一名成绩,班主任决定开一场庆功会,为此拨款购买奖品犒劳运动员。期望拨款金额能购买最大价值的奖品,可以补充他们的精力和体力。

【输入】
第一行二个数n(n≤500),m(m≤6000),其中n代表希望购买的奖品的种数,m表示拨款金额。

接下来n行,每行3个数,v、w、s,分别表示第I种奖品的价格、价值(价格与价值是不同的概念)和能购买的最大数量(买0件到s件均可),其中v≤100,w≤1000,s≤10。

【输出】
一行:一个数,表示此次购买能获得的最大的价值(注意!不是价格)。

【输入样例】
5 1000
80 20 4
40 50 9
30 50 7
40 30 6
20 20 1
【输出样例】
1040

#include
#include
#include
#include
#include
#include
#define INF 0x3f3f3f3f
#define N 10001
using namespace std;
int m,n;
int w[N],c[N],num[N],f[N];
void MultiplePack(int cost,int weight,int num)
{
    for(int j=m;j>=0;j--)
        for(int k=0;k<=num;k++)
            if(j-k*weight>=0)
                f[j]=max(f[j],f[j-k*weight]+k*cost);
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        cin>>w[i]>>c[i]>>num[i];
    for(int i=1;i<=n;i++)
        MultiplePack(c[i],w[i],num[i]);
    cout<<f[m]<<endl;
    return 0;

六、分治算法

二分

【概述】
二分查找又称折半查找,其要求线性表中的记录必须按关键码有序,且必须采用顺序存储。

其基本思想是:用给定值 k 先与中间结点的关键字比较,中间结点把线形表分成两个子表,若相等则查找成功;若不相等,再根据 k 与该中间结点关键字的比较结果确定下一步查找哪个子表,这样递归进行,直到查找到或查找结束发现表中没有这样的结点。
【经典例题】
The SUM problem can be formulated as follows: given four lists A, B, C, D of integer values, compute how many quadruplet (a, b, c, d ) ∈ A x B x C x D are such that a + b + c + d = 0 . In the following, we assume that all lists have the same size n .

Input
The first line of the input file contains the size of the lists n (this value can be as large as 4000). We then have n lines containing four integer values (with absolute value as large as 2 28 ) that belong respectively to A, B, C and D .

Output
For each input file, your program has to write the number quadruplets whose sum is zero.

Sample Input
6
-45 22 42 -16
-41 -27 56 30
-36 53 -37 77
-36 30 -75 -46
26 -38 -10 62
-32 -54 -6 45
Sample Output
5

Hint
Sample Explanation: Indeed, the sum of the five following quadruplets is zero: (-45, -27, 42, 30), (26, 30, -10, -46), (-32, 22, 56, -46),(-32, 30, -75, 77), (-32, -54, 56, 30).

题意:
给你一些数,每行4个,从每列里选一个数,让这四个数的和为0,问有多少种取法

思路:
先分别求出前两列后两列的所有的和,然后对后两列的和进行排序,对后两列进行二分查找看有多少种情况即可,枚举会超时

代码实现:

#include 
#include 
#include 
#include 
using namespace std;
int a[4002][4], sum1[16000002], sum2[16000002];
int main()
{
    int n, mid;
    while (cin>>n)
    {
        for (int i = 0; i < n; i++)
        {
            scanf("%d%d%d%d", &a[i][0], &a[i][1], &a[i][2], &a[i][3]);
        }
        int k = 0;
        int m = 0;
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++)
            {
                sum1[k++] = a[i][0] + a[j][1];
                sum2[m++] = a[i][2] + a[j][3];
            }
        sort(sum2, sum2 + k);
        int cnt = 0;
        for (int i = 0; i < k; i++)
        {
            int left = 0;
            int right = k - 1;
            while (left <= right)
            {
                mid = (left + right) / 2;
                if (sum1[i] + sum2[mid] == 0)
                {
                    cnt++;
                    for (int j = mid + 1; j < k; j++)
                    {
                        if (sum1[i] + sum2[j] != 0)
                            break;
                        else
                            cnt++;
                    }
                    for (int j = mid - 1; j >= 0; j--)
                    {
                        if (sum1[i] + sum2[j] != 0)
                            break;
                        else
                            cnt++;
                    }
                    break;
                }
                if (sum1[i] + sum2[mid] < 0)
                    left = mid + 1;
                else
                    right = mid - 1;
            }
        }
        printf("%d\n", cnt);
    }
    return 0;
}

F题
Given N numbers, X1, X2, … , XN, let us calculate the difference of every pair of numbers: ∣Xi - Xj∣ (1 ≤ i < j ≤ N). We can get C(N,2) differences through this work, and now your task is to find the median of the differences as quickly as you can!

Note in this problem, the median is defined as the (m/2)-th smallest number if m,the amount of the differences, is even. For example, you have to find the third smallest one in the case of m = 6.

Input
The input consists of several test cases.
In each test case, N will be given in the first line. Then N numbers are given, representing X1, X2, … , XN, ( Xi ≤ 1,000,000,000 3 ≤ N ≤ 1,00,000 )

Output
For each test case, output the median in a separate line.

Sample Input
4
1 3 2 4
3
1 10 2
Sample Output
1
8

题意:
题意比较好理解,就是一个数组每两个数相减取绝对值,求所有绝对值组成的数组的中数

思路:
使用了二分的方法,直接暴力解决会超时,判断小于等于mid的数是否大于所有绝对值个数的一半,这里用了STL中的upper_bound函数
参考博客:upper_bound函数资料

代码

#include
#include
#include
#define ll long long
using namespace std;
const int maxn = 1e5 + 5;
int a[maxn];
int n, m;


bool check(int x)
{
    int sum = 0;
    for (int i = 0; i < n - 1; i++)
    {
        int p = upper_bound(a, a + n, a[i] + x) - a;
        sum += p - i - 1;
    }
    return sum >= (m + 1) / 2;
}

int main()
{
    while (cin >> n)
    {
        for (int i = 0; i < n; i++)
            scanf("%d", &a[i]);
        sort(a, a + n);
        m = n * (n - 1) / 2;
        int l = 0, r = a[n - 1] - a[0], ans = (l + r) / 2;
        while (l <= r)
        {
            int mid = (l + r) / 2;
            if (check(mid)) ans = mid, r = mid - 1;
            else l = mid + 1;
        }
        cout << ans << endl;
    }
    return 0;
}

G题
Inhabitants of the Wonderland have decided to hold a regional programming contest. The Judging Committee has volunteered and has promised to organize the most honest contest ever. It was decided to connect computers for the contestants using a “star” topology - i.e. connect them all to a single central hub. To organize a truly honest contest, the Head of the Judging Committee has decreed to place all contestants evenly around the hub on an equal distance from it.
To buy network cables, the Judging Committee has contacted a local network solutions provider with a request to sell for them a specified number of cables with equal lengths. The Judging Committee wants the cables to be as long as possible to sit contestants as far from each other as possible.
The Cable Master of the company was assigned to the task. He knows the length of each cable in the stock up to a centimeter,and he can cut them with a centimeter precision being told the length of the pieces he must cut. However, this time, the length is not known and the Cable Master is completely puzzled.
You are to help the Cable Master, by writing a program that will determine the maximal possible length of a cable piece that can be cut from the cables in the stock, to get the specified number of pieces.

Input
The first line of the input file contains two integer numb ers N and K, separated by a space. N (1 = N = 10000) is the number of cables in the stock, and K (1 = K = 10000) is the number of requested pieces. The first line is followed by N lines with one number per line, that specify the length of each cable in the stock in meters. All cables are at least 1 meter and at most 100 kilometers in length. All lengths in the input file are written with a centimeter precision, with exactly two digits after a decimal point.

Output
Write to the output file the maximal length (in meters) of the pieces that Cable Master may cut from the cables in the stock to get the requested number of pieces. The number must be written with a centimeter precision, with exactly two digits after a decimal point.
If it is not possible to cut the requested number of pieces each one being at least one centimeter long, then the output file must contain the single number “0.00” (without quotes).

Sample Input
4 11
8.02
7.43
4.57
5.39
Sample Output
2.00
题意:
有n段长度分别为Li的电缆,要求把它们分割成K条长度为X的电缆,问X的最大值为多少。

题解:
将X视为变量,可知它的范围为0~max; 那么问题就变成了电缆长度取X时,所得的电缆条数大于,还是等于,或小于K的问题。 用二分查找法能很快的找出K的值,不过要注意向下取整。
代码

#include
#include
#include
#define ll long long
using namespace std;
const int maxn = 1e5 + 5;
int a[maxn];
int n, m;


bool check(int x)
{
    int sum = 0;
    for (int i = 0; i < n - 1; i++)
    {
        int p = upper_bound(a, a + n, a[i] + x) - a;
        sum += p - i - 1;
    }
    return sum >= (m + 1) / 2;
}

int main()
{
    while (cin >> n)
    {
        for (int i = 0; i < n; i++)
            scanf("%d", &a[i]);
        sort(a, a + n);
        m = n * (n - 1) / 2;
        int l = 0, r = a[n - 1] - a[0], ans = (l + r) / 2;
        while (l <= r)
        {
            int mid = (l + r) / 2;
            if (check(mid)) ans = mid, r = mid - 1;
            else l = mid + 1;
        }
        cout << ans << endl;
    }
    return 0;
}

三分

【概述】
三分法是二分法的扩展,其用于求凸性或凹形函数的极值,当通过函数本身表达式并不容易求解时,就可以用三分查找来不断逼近求解。

其原理为:函数中存在一个点 x 是最值,对于 x 的左边,满足单调上升(下降),右边满足单调下降(上升),然后进行两次二分,使得不断的逼近这个 x 点,最后求得答案。

【基本思想】
类似二分的定义 Left 和 Right,对于 [L,R],先找出 lmid,紧接着再找出 rmid,然后比较两者谁更优,然后舍去不优的。

mid=Left+(Left-Right)/3
mid_mid=Right-(Right-Left)/3
若 lmid 靠近极值点,则 Right=rmid,若 rmid 靠近极值点,则 Left=lmid;

ACMer必看的基础算法(附经典例题)_第2张图片

要注意的是,若在三分过程中,遇到 f(lmid)=f(rmid) 时,若函数严格单调,那么令 left=lmid 或令 right=rmid 均可,若函数不严格单调,即在函数中存在一段函数值相等的区间,将无法判断定义域左右边界该如何缩小,三分法也就不再适用。

【经典例题】
1.曲线
【题目描述】
明明做作业的时候遇到了n个二次函数Si(x)= ax2 + bx + c,他突发奇想设计了一个新的函数F(x) = max(Si(x)), i = 1…n.

明明现在想求这个函数在[0,1000]的最小值,要求精确到小数点后四位四舍五入。

【输入】
输入包含T 组数据 (T < 10) ,每组第一行一个整数 n(n ≤ 10000) ,之后n行,每行3个整数a (0 ≤ a ≤ 100), b (|b| ≤ 5000), c (|c| ≤ 5000) ,用来表示每个二次函数的3个系数,注意二次函数有可能退化成一次。

【输出】
每组数据一个输出,表示新函数F(x)的在区间[0,1000]上的最小值。精确到小数点后四位,四舍五入。

【输入样例】
2
1
2 0 0
2
2 0 0
2 -4 2

【输出样例】
0.0000
0.5000

AC代码

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define PI acos(-1.0)
#define INF 0x3f3f3f3f
#define LL long long
#define Pair pair
const double EPS = 1E-12;
const int MOD = 1E9+7;
const int N = 1000000+5;
const int dx[] = {-1,1,0,0,-1,-1,1,1};
const int dy[] = {0,0,-1,1,-1,1,-1,1};
using namespace std;
int n;
double a[N],b[N],c[N];
double cal(double x){
    double maxx=-INF;
    for(int i=1;i<=n;i++)
        maxx=max(maxx,a[i]*x*x+b[i]*x+c[i]);
    return maxx;
}
int main() {
    int t;
    scanf("%d",&t);
    while(t--){
        scanf("%d",&n);
        for(int i=1;i<=n;i++)
            scanf("%lf%lf%lf",&a[i],&b[i],&c[i]);
        double left=0,right=1000;
        while(right-left>=EPS){
            double lmid=left+(right-left)/3.0;
            double rmid=right-(right-left)/3.0;
            if(cal(lmid)<=cal(rmid))
                right=rmid;
            else
                left=lmid;
        }
        printf("%.4lf\n",cal(left));
    }
 
    return 0;
}

七、补充

STL的简单应用

1.string类

string类是c++中的字符串类型。
【头文件】

	#include

【输入方式】
主要输入方式有两种,分别为cin和getline

1.cin

	string s;
	cin>>s;

注:cin输入的字符串不能含空格

2.getline

	string s;
	getline(cin,s);

【各种简单操作】

	string s1, s2;			//定义两个字符串对象
	string s3 = "Hello, World!";	//创建s3,并初始化
	string s4("I am ");
	s2 = "Today";				//赋值
	s1 = s3 + " " + s4;			//字符串连接
	s1 += " 5 ";				//末尾追加
	cout << s1 + s2 + "!" <<endl;	//输出字符串内容

【常用功能函数】

	string s;
	s.size()//返回字符串的长度
	s.empty()//判断字符串是否为空 若空,返回ture

在string对象中,可用运算符比较两个字符串的大小
两个string相等意味着它们的长度相同,并且所包含的字符也完全相同
字符串的大小关系依照字典顺序定义且区分大小写字母

	string s1 = "hello";		
	string s2 = "hello world";	// s2 > s1
	string s3 = "Hello";		// s3 < s1, s3 < s2

像字符数组那样,下标运算符可以访问string对象中指定位置的字符

2.栈(stack)
【描述】
栈是一种特殊的数据结构,遵循先进后出的原则,只能在栈顶操作元素

【头文件】

	#include

【定义】

stack stack_name

	stack<int>s;

【基本功能函数】

	empty() //返回bool型,表示栈内是否为空 (s.empty() )
	size() // 返回栈内元素个数 (s.size() )
	top() //返回栈顶元素值 (s.top() )
	pop() // 移除栈顶元素(s.pop(); )
	push(data_type a) // 向栈压入一个元素 a(s.push(a);

3.队列(queue)
【描述】
队列是一种先进先出的数据结构,从底端加入元素,从顶端取出元素

【头文件】

	#include

【定义】
queuequeue_name

例如

queue<int>q;

【常用功能函数】

	empty() //返回bool型,表示queue是否为空 (q.empty() )
	size() //返回queue内元素个数 (q.size() )
	front() //返回queue内的下一个元素 (q.front() )
	back() // 返回queue内的最后一个元素(q.back() )
	pop() // 移除queue中的一个元素(q.pop(); )
	push(data_type a) // 将一个元素a置入queue中(q.push(a)

4.动态数组(vector)

【头文件】

	#include

【定义】
vectorvector_name

例如

	vector<int>v;

【常用功能函数】

	empty() // 返回bool型,表示vector是否为空 (v.empty() )
	size() // 返回vector内元素个数 (v.size() )
	push_back(data_type a) //将元素a插入最尾端
	pop_back() //将最尾端元素删除
	v[i] //类似数组取第i个位置的元素(v[0])

5.优先队列(priority_queue)
【描述】
优先队列是一个拥有权值观念的queue,自动依照元素的权值排列,权值最高排在前面

【头文件】

	#include

【定义】
priority_queue priority_queue_name

例如

	priority_queue<int>q

【常用功能函数】

	q.push(elem) //将元素elem置入优先队列
	q.top() //返回优先队列的下一个元素
	q.pop() //移除一个元素
	q.size() //返回队列中元素的个数
	q.empty() //返回优先队列是否为空

结构体

【描述】
结构体是数目固定、类型可不同的数据项组合项组合成的一个有机的整体;每个数据项称为一个成员

结构体

【定义】
1.定义结构体类型的同时定义变量
struct 结构体类型名{

成员类型 成员名;

} 结构体变量表;

例如

struct student{
char name;
int chinese,math,english;
int total;
}student1,student2;

2.先定义结构体再定义结构体变量
struct 结构体类型名{

成员类型 成员名;

};
结构体名 结构体变量;

例如

struct student{
char name;
int chinese, math;
int total;
};
student student1,student2;

注:
a.成员类型名可以是基本形或构造型;成员名可以与程序中的其他变量重名,互不干扰
b.结构体变量名和结构体名不能相同
c.定义结构体时,系统不分配实际内存;只有定义结构体变量时,系统才分配内存

【引用】

#include
using namespace std;
struct student {
	char name;
	int chinese, math, english;
	int total;
}stu1,stu2;
int main()
{
	cin >> stu1.name;
	cin >> stu1.chinese >> stu1.math >> stu1.english;
	cin >> stu1.total;
	//...
}

结构体数组

结构体数组的每个数组元素都是结构体类型的数据

【结构体数组的定义】
和普通结构体相同,也有两种定义方式

例如

struct student{
char name[20];
int age;
char adress[30];
}stu[10];
struct student{
char name[20];
int age;
char adress[30];
}
student stu[2]
={{"张三",18,"山东省"},
{"李四",18,"山东省"}};//初始化

注:
全部初始化时可省数组维数
结构体数组在内存中连续存放
2.结构体数组元素的引用

【引用方式】
结构体数组名[下标].成员名

eg.

#include
using namespace std;
struct student {
	char name[20];
	int age;
	char adress[30];
}stu[10];
int main()
{
	for (int i = 1; i <= 10; i++)
		cin >> stu[i].name >> stu[i].age >> stu[i].adress;
	//...
}

本人花费了两天时间来整理这篇文章,供大家参考,如有错误,请指正

留个赞吧~

你可能感兴趣的:(ACMer必看的基础算法(附经典例题))