学习笔记——动态规划(全)

递推

1.递推和动态规划有什么关系?

递推问题包括动态规划,动态规划一定是递推,递推不一定是动态规划。
动态规划是一种决策性的问题,是在状态中做最优决策的一种特殊递推算法,通常的问法包括求最大最小值等,而递推可能还会包括求种类数等问题。

2.递推和递归的区别?

递推是一种算法,用来解决一类特殊的问题,而递归是程序实现的形式,不属于算法范畴。

3.递推问题求解的一般过程

1.状态定义(核心环节,f[i][j]:符号表达式以及对这个表达式的文字定义)
2.确定递推公式(形如dp [ i ][ j ]=dp [ i-1 ][ j ]+dp [ i ][ j-1 ])
3.边界条件的确定(例如发dp [ 0 ][ 0 ]=0)
4.程序实现(包括递归加记忆化 以及循环两种实现方式,后者通常更常用)

4.例题

1.兔子繁殖

题目描述
如果有一对小兔,每一个月都生下一对小兔,而所生下的每一对小兔在出生后的第三个月也都生下一对小兔。那么,由一对兔子开始,n 个月后有多少对小兔子呢?

输入
输入一个数字 n(1≤n≤100),代表题目中询问的月份。

输出
对于每个询问,输出一行整数,代表 n 月的时候,小兔子的数量。

样例输入1

4

样例输出1

5

样例输入2

65

样例输出2

27777890035288


状态定义: dp [ i ]代表第i天兔子的数量。
递推公式: dp [ i ]=f [ i-1 ]+f [ i-2 ]。
其中dp [ i-1 ]为第i天老兔子的数量,dp [ i-2 ]为第i天小兔子的数量。
边界条件: dp [ 1 ]=1,dp [ 2 ]=2。
程序实现:

#include<iostream>
#include<vector>
using namespace std;
#define MAXSIZE 100
//本题使用大整数版本来实现 
class BigInt:public vector<int>
{
	public:
		BigInt(){push_back(0);}
		BigInt(int x){
			push_back(x);
			proccess_digit();
		}
		BigInt &operator+=(const BigInt&a)
		{
			for(int i=0;i<a.size();i++)
			{
				if(i>=size())push_back(a[i]);
				else at(i)+=a[i];
			}
			proccess_digit();
			return*this;
		}
		BigInt operator+(const BigInt&a)
		{
			BigInt ret(*this);
			ret+=a;
			return ret;
		}
		void proccess_digit()
		{
			for(int i=0;i<size();i++)
			{
				if(at(i)<10)continue;
				if(i==size()-1)push_back(0);
				at(i+1)+=at(i)/10;
				at(i)%=10;
			}
			return ;
		}
};
BigInt dp[MAXSIZE+5];
ostream&operator<<(ostream &out,BigInt &a)
{
	for(int i=a.size()-1;i>=0;i--)
	out<<a[i];
	return out;
}
int main()
{
	int n;
	cin>>n;
	dp[1]=1;
	dp[2]=2;
	for(int i=3;i<=n;i++)
	dp[i]=dp[i-1]+dp[i-2];
	cout<<dp[n];
	return 0;
}


2.钱币问题

题目描述
某个国家的货币系统中,有 m 种面额的钱币,现在要用这 m 种面额的钱币凑足 n 元钱,问一共有多少种方法。m 种钱币不一定要都被用到。

例如,有 3 种钱币,分别为1、2、5,那么有四种方法拼凑出5元钱

(1,1,1,1,1) 全是1元钱
(1,2,2),(1,1,1,2) 使用1元和2元
(5) 只用5元钱
注意:方案中的钱币不分顺序,也就是说(1,2,2) 和(2,1,2)是同一种方法。

输入
输入两个数字 m, n(1≤m≤20,200≤n≤10000),第二行 m 个数字,代表 m 种钱币的面额,钱币面额大于0,数据中保证 m 种钱币各不相同。

输出
输出一个整数,代表拼凑出 n 元钱的方法数,答案有可能过大,请对 9973 取余。

样例输入1

8 200
1 2 5 10 20 50 100 200

样例输出1

3871


递推状态: dp[ i ][ j ]:选择前i种钱币,组成j元钱的方法总数。
递推公式: dp [ i ][ j ]=dp [ i-1 ][ j ]+dp [ i ][ j-money[ i ] ]。
dp [ i-1 ][ j ]为不使用第i种钱币的方法总数,dp [ i ][ j-money[ i ] ]为使用第i种钱币的方法总数,两者构成了 f [ i ][ j ]的全集。
边界条件: dp [ i ][ 0 ]=1,dp [ 0 ][ j ]=0。
代码实现:

#include<iostream>
using namespace std;
#define MAX_M 20
#define MAX_N 10000
int dp[MAX_M+5][MAX_N+5]={0};
int money[MAX_M+5];
int main()
{
	int m,n;
	cin>>m>>n;
	for(int i=1;i<=m;i++)
	cin>>money[i];
	for(int i=1;i<=m;i++)
	{
		dp[i][0]=1;
		for(int j=1;j<=n;j++)
		{
			dp[i][j]=dp[i-1][j];
			if(j<money[i])continue;
			dp[i][j]+=dp[i][j-money[i]];
			dp[i][j]%=9973;
		}
	}
	cout<<dp[m][n];
	return 0;
 } 


3.爬楼梯

题目描述
小海是一个顽皮的少年,对于爬楼梯这种事情,他从来都不愿意一步一步走,每次上楼梯的时候,要么往上跨两级,要么往上跨三级。对于有 n 级台阶的楼梯,小海想知道他从最下面走到最上面的方法总数。

在这里插入图片描述

输入
输入一个数字 n(1≤n≤500),代表台阶总数。

输出
输出一个整数,代表小海从最下面走到最上面的方法总数。

样例输入1

5

样例输出1

2


递推状态: dp[ i ]:走到第i层的方法总数。
递推公式: dp[ i ]=dp[ i-2 ]+dp[ i-3 ]。
走到第i层的方法总数为从i-2层走到i以及从i-3层走到i层的方法之和。
边界条件: dp[ 0 ]=1,dp[ 1 ]=0,dp[ 2 ]=1。
代码实现:

#include<iostream>
#include<vector>
using namespace std;
#define MAXSIZE 500
//本题使用大整数版本来实现 
class BigInt:public vector<int>
{
	public:
		BigInt(){push_back(0);}
		BigInt(int x){
			push_back(x);
			proccess_digit();
		}
		BigInt &operator+=(const BigInt&a)
		{
			for(int i=0;i<a.size();i++)
			{
				if(i>=size())push_back(a[i]);
				else at(i)+=a[i];
			}
			proccess_digit();
			return*this;
		}
		BigInt operator+(const BigInt&a)
		{
			BigInt ret(*this);
			ret+=a;
			return ret;
		}
		void proccess_digit()
		{
			for(int i=0;i<size();i++)
			{
				if(at(i)<10)continue;
				if(i==size()-1)push_back(0);
				at(i+1)+=at(i)/10;
				at(i)%=10;
			}
			return ;
		}
};
ostream&operator<<(ostream &out,BigInt &a)
{
	for(int i=a.size()-1;i>=0;i--)
	out<<a[i];
	return out;
}
BigInt dp[MAXSIZE+5];
int main()
{
	int n;
	cin>>n;
	dp[0]=1;dp[1]=0;dp[2]=1;
	for(int i=3;i<=n;i++)
	dp[i]=dp[i-2]+dp[i-3];
	cout<<dp[n];
	return 0;
}


4.墙壁涂色

题目描述
给一个环形的墙壁涂颜色,颜色一共有 k 种,墙壁被竖直地划分成 n 个部分,相邻的部分颜色不能相同。请你写程序计算出一共有多少种给墙壁上色的方案?

例如,当 n=5,k=3​ 时,下面是一种合法的涂色方案

在这里插入图片描述

而由于墙壁是环形的,所以下面就是一种非法的方案

在这里插入图片描述

输入
输入两个数字 n,k(1≤n≤103,2≤k≤10),分别代表墙壁数量和颜色种类。

输出
对于每个询问,输出一行整数,合法的墙壁涂色方案数。

样例输入1

5 3

样例输出1

30


递推状态: dp[ n ][ i ][ j ]:以i开头,以j结尾长度为n的情况总数。(非最优定义)
递推公式: dp[ n ][ i ][ j ]=sum(dp[ n-1 ][ i ][ k ])(k!=j)
以i开头,以j结尾长度为n的情况总数,为长度为n-1,且最后一位不为j的情况之和。
边界条件: dp[ 1 ][ i ][ i ]=1,dp[ 2 ][ i ][ j ]=1(i!=j)。
代码实现:

#include<iostream>
#include<vector>
using namespace std;
#define MAX_N 1000
#define MAX_K 10
//本题使用大整数版本来实现 
class BigInt:public vector<int>
{
	public:
		BigInt(){push_back(0);}
		BigInt(int x){
			push_back(x);
			proccess_digit();
		}
		BigInt &operator+=(const BigInt&a)
		{
			for(int i=0;i<a.size();i++)
			{
				if(i>=size())push_back(a[i]);
				else at(i)+=a[i];
			}
			proccess_digit();
			return*this;
		}
		BigInt operator+(const BigInt&a)
		{
			BigInt ret(*this);
			ret+=a;
			return ret;
		}
		void proccess_digit()
		{
			for(int i=0;i<size();i++)
			{
				if(at(i)<100000)continue;
				if(i==size()-1)push_back(0);
				at(i+1)+=at(i)/100000;
				at(i)%=100000;
			}
			return ;
		}
};
ostream&operator<<(ostream &out,BigInt &a)
{
	out<<a[a.size()-1]; 
	for(int i=a.size()-2;i>=0;i--)
	{
		for(int j=10000;j>0;j/=10)
		{
			out<<a[i]%(j*10)/j;
		}
	}
	return out;
}
BigInt dp[2][MAX_K+5][MAX_K+5]={0};//滚动数组避免内存超限 
int main()
{
	int n,k;
	cin>>n>>k;
	for(int i=1;i<=k;i++)dp[1][i][i]=1;
	for(int i=1;i<=k;i++)
	{
		for(int j=1;j<=k;j++)
		{
			if(i!=j)dp[0][i][j]=1;
		}
	}
	for(int s=3;s<=n;s++)
	{
		for(int i=1;i<=k;i++)
		{
			for(int j=1;j<=k;j++)
			{
				dp[s%2][i][j]=0;
				for(int l=1;l<=k;l++)
				{	
					if(l==j)continue;
					dp[s%2][i][j]+=dp[(s-1)%2][i][l];
				}
			}
		}
	}
	BigInt ans=0;
	for(int i=1;i<=k;i++)
	{
		for(int j=1;j<=k;j++)
		{
			if(i==j)continue;
			ans+=dp[n%2][i][j]; 
		}
	}
	cout<<ans;
	return 0;
}


5.数的划分

题目描述

将整数 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.


递推状态: dp[ i ][ j ]:整数i被分成j份的方法总数。
递推公式: dp[ i ][ j ]=dp[ i-1 ][ j-1 ]+dp[ i-j ][ j ]。
整数i被分成j份的方法总数包含含有1的方法总数与不含有1的方法总数。
边界条件: dp[ i ][ 1 ]=1,dp[ 0 ][ 0 ]=1.。
代码实现:

#include<iostream>
using namespace std;
#define MAX_N 200
#define MAX_K 6
int dp[MAX_N+5][MAX_K+5]={0};
int main()
{
	int n,k;
	cin>>n>>k;
	dp[0][0]=1;
	for(int i=1;i<=n;i++)
	{
		dp[i][1]=1;
		for(int j=2;j<=min(i,k);j++)
		{
			dp[i][j]=dp[i-1][j-1]+dp[i-j][j];
		}
	}
	cout<<dp[n][k];
	return 0;
 } 


6.数的计算

题目描述

给出正整数 n n n,要求按如下方式构造数列:

  1. 只有一个数字 n n n 的数列是一个合法的数列。
  2. 在一个合法的数列的末尾加入一个正整数,但是这个正整数不能超过该数列最后一项的一半,可以得到一个新的合法数列。

请你求出,一共有多少个合法的数列。两个合法数列 a , b a, b a,b 不同当且仅当两数列长度不同或存在一个正整数 i ≤ ∣ a ∣ i \leq |a| ia,使得 a i ≠ b i a_i \neq b_i ai=bi

输入格式

输入只有一行一个整数,表示 n n n

输出格式

输出一行一个整数,表示合法的数列个数。

样例 #1

样例输入 #1

6

样例输出 #1

6

提示

样例 1 解释

满足条件的数列为:

  • 6 6 6
  • 6 , 1 6, 1 6,1
  • 6 , 2 6, 2 6,2
  • 6 , 3 6, 3 6,3
  • 6 , 2 , 1 6, 2, 1 6,2,1
  • 6 , 3 , 1 6, 3, 1 6,3,1

数据规模与约定

对于全部的测试点,保证 1 ≤ n ≤ 1 0 3 1 \leq n \leq 10^3 1n103


递推状态: dp[ i ]:以i开始的数的合法序列数。
递推公式: dp[ i ]=sum(dp[ j ])+1 (i/2>=j>=1)
dp[ i ]包括以i结尾的序列数1与以j开头的序列数的和。
代码实现:

#include<iostream>
using namespace std;
#define MAXSIZE 1000
int dp[MAXSIZE+5];
int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		dp[i]=1;
		for(int j=1;j<=i/2;j++)	
		dp[i]+=dp[j];
	}
	cout<<dp[n];
	return 0;
}


7.神经网络

题目背景

人工神经网络(Artificial Neural Network)是一种新兴的具有自我学习能力的计算系统,在模式识别、函数逼近及贷款风险评估等诸多领域有广泛的应用。对神经网络的研究一直是当今的热门方向,兰兰同学在自学了一本神经网络的入门书籍后,提出了一个简化模型,他希望你能帮助他用程序检验这个神经网络模型的实用性。

题目描述

在兰兰的模型中,神经网络就是一张有向图,图中的节点称为神经元,而且两个神经元之间至多有一条边相连,下图是一个神经元的例子:

神经元(编号为 i i i

图中, X 1 ∼ X 3 X_1 \sim X_3 X1X3 是信息输入渠道, Y 1 ∼ Y 2 Y_1 \sim Y_2 Y1Y2 是信息输出渠道, C i C_i Ci 表示神经元目前的状态, U i U_i Ui 是阈值,可视为神经元的一个内在参数。

神经元按一定的顺序排列,构成整个神经网络。在兰兰的模型之中,神经网络中的神经元分为几层;称为输入层、输出层,和若干个中间层。每层神经元只向下一层的神经元输出信息,只从上一层神经元接受信息。下图是一个简单的三层神经网络的例子。

兰兰规定, C i C_i Ci 服从公式:(其中 n n n 是网络中所有神经元的数目)

C i = ( ∑ ( j , i ) ∈ E W j i C j ) − U i C_i=\left(\sum\limits_{(j,i) \in E} W_{ji}C_{j}\right)-U_{i} Ci= (j,i)EWjiCj Ui

公式中的 W j i W_{ji} Wji(可能为负值)表示连接 j j j 号神经元和 i i i 号神经元的边的权值。当 C i C_i Ci 大于 0 0 0 时,该神经元处于兴奋状态,否则就处于平静状态。当神经元处于兴奋状态时,下一秒它会向其他神经元传送信号,信号的强度为 C i C_i Ci

如此.在输入层神经元被激发之后,整个网络系统就在信息传输的推动下进行运作。现在,给定一个神经网络,及当前输入层神经元的状态( C i C_i Ci),要求你的程序运算出最后网络输出层的状态。

输入格式

输入文件第一行是两个整数 n n n 1 ≤ n ≤ 100 1 \le n \le 100 1n100)和 p p p。接下来 n n n 行,每行 2 2 2 个整数,第 i + 1 i+1 i+1 行是神经元 i i i 最初状态和其阈值( U i U_i Ui),非输入层的神经元开始时状态必然为 0 0 0。再下面 p p p 行,每行有两个整数 i , j i,j i,j 及一个整数 W i j W_{ij} Wij,表示连接神经元 i , j i,j i,j 的边权值为 W i j W_{ij} Wij

输出格式

输出文件包含若干行,每行有 2 2 2 个整数,分别对应一个神经元的编号,及其最后的状态, 2 2 2 个整数间以空格分隔。仅输出最后状态大于 0 0 0 的输出层神经元状态,并且按照编号由小到大顺序输出。

若输出层的神经元最后状态均小于等于 0 0 0,则输出 NULL

样例 #1

样例输入 #1

5 6
1 0
1 0
0 1
0 1
0 1
1 3 1
1 4 1
1 5 1
2 3 1
2 4 1
2 5 1

样例输出 #1

3 1
4 1
5 1

代码实现

#include<iostream>
#include<vector>
#include<queue>
using namespace std;
#define MAX_N 100
int w[MAX_N+5][MAX_N+5]={0};
int u[MAX_N+5],c[MAX_N+5];
int indeg[MAX_N+5]={0},outdeg[MAX_N+5]={0};
vector<int>after[MAX_N+5]; 
int main()
{
	int n,p;
	cin>>n>>p;
	for(int i=1;i<=n;i++)
	cin>>c[i]>>u[i];
	for(int i=1,a,b,W;i<=p;i++)
	{
		cin>>a>>b>>W;
		indeg[b]+=1;
		outdeg[a]+=1;
		w[a][b]=W;
		after[a].push_back(b);
	}
	for(int i=1;i<=n;i++)
	if(indeg[i]!=0)c[i]-=u[i];
	queue<int>q;
	for(int i=1;i<=n;i++)
	if(indeg[i]==0)q.push(i);
	while(!q.empty())//拓扑序 
	{
		int fro=q.front();
		q.pop();
		for(int i=0;i<after[fro].size();i++)
		{
			int to=after[fro][i];
			if(c[fro]>0)c[to]+=c[fro]*w[fro][to];
			indeg[to]--;
			if(indeg[to]==0)q.push(to);
		}
	}
	int flag=1;
	for(int i=1;i<=n;i++)
	{
		if(outdeg[i])continue;
		if(c[i]<=0)continue;
		cout<<i<<" "<<c[i]<<endl;
		flag=0;
	}
	if(flag)cout<<"NULL"<<endl;
	return 0;
}


8.栈

题目背景

栈是计算机中经典的数据结构,简单的说,栈就是限制在一端进行插入删除操作的线性表。

栈有两种最重要的操作,即 pop(从栈顶弹出一个元素)和 push(将一个元素进栈)。

栈的重要性不言自明,任何一门数据结构的课程都会介绍栈。宁宁同学在复习栈的基本概念时,想到了一个书上没有讲过的问题,而他自己无法给出答案,所以需要你的帮忙。

题目描述

宁宁考虑的是这样一个问题:一个操作数序列, 1 , 2 , … , n 1,2,\ldots ,n 1,2,,n(图示为 1 到 3 的情况),栈 A 的深度大于 n n n

现在可以进行两种操作,

  1. 将一个数,从操作数序列的头端移到栈的头端(对应数据结构栈的 push 操作)
  2. 将一个数,从栈的头端移到输出序列的尾端(对应数据结构栈的 pop 操作)

使用这两种操作,由一个操作数序列就可以得到一系列的输出序列,下图所示为由 1 2 3 生成序列 2 3 1 的过程。

(原始状态如上图所示)

你的程序将对给定的 n n n,计算并输出由操作数序列 1 , 2 , … , n 1,2,\ldots,n 1,2,,n 经过操作可能得到的输出序列的总数。

输入格式

输入文件只含一个整数 n n n 1 ≤ n ≤ 18 1 \leq n \leq 18 1n18)。

输出格式

输出文件只有一行,即可能输出序列的总数目。

样例 #1

样例输入 #1

3

样例输出 #1

5

递推状态: dp[ i ]:i个数的出栈序列数。
递推公式: dp[ i ]=sum(dp[ j-1 ]*dp[ n-j ]) (1<=j<=i)
前i个数的出栈序列数为以第j个数为最后一个出栈元素的序列总和。
边界条件: dp[ 0 ]=1
代码实现:

#include<iostream>
using namespace std;
#define MAX_N 18
int dp[MAX_N+5]={0};
int main()
{
	int n;
	cin>>n;
	dp[0]=1;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=i;j++)
		{
			int x=dp[j-1]*dp[i-j];
			dp[i]+=x;
		}
	}
	cout<<dp[n];
	return 0;
 } 


9.循环

题目描述

乐乐是一个聪明而又勤奋好学的孩子。他总喜欢探求事物的规律。一天,他突然对数的正整数次幂产生了兴趣。

众所周知, 2 2 2 的正整数次幂最后一位数总是不断的在重复 2 , 4 , 8 , 6 , 2 , 4 , 8 , 6 … 2,4,8,6,2,4,8,6… 2,4,8,6,2,4,8,6 我们说 2 2 2 的正整数次幂最后一位的循环长度是 4 4 4(实际上 4 4 4 的倍数都可以说是循环长度,但我们只考虑最小的循环长度)。类似的,其余的数字的正整数次幂最后一位数也有类似的循环现象:

数字 循环 循环长度 2 2 , 4 , 8 , 6 4 3 3 , 9 , 7 , 1 4 4 4 , 6 2 5 5 1 6 6 1 7 7 , 9 , 3 , 1 4 8 8 , 4 , 2 , 6 4 9 9 , 1 2 \def\arraystretch{1.5} \begin{array}{c|c|c}\hline \textbf{数字}& \textbf{循环} & \textbf{循环长度} \cr\hline\hline 2 & 2,4,8,6 & 4\cr\hline 3 & 3,9,7,1 & 4\cr\hline 4 & 4,6 & 2\cr\hline 5 & 5 & 1\cr\hline 6 & 6 & 1\cr\hline 7 & 7,9,3,1 & 4\cr\hline 8 & 8,4,2,6 & 4\cr\hline 9 & 9,1 & 2\cr\hline \end{array} 数字23456789循环2,4,8,63,9,7,14,6567,9,3,18,4,2,69,1循环长度44211442

这时乐乐的问题就出来了:是不是只有最后一位才有这样的循环呢?对于一个整数 n n n 的正整数次幂来说,它的后 k k k 位是否会发生循环?如果循环的话,循环长度是多少呢?

注意:

  1. 如果 n n n 的某个正整数次幂的位数不足 k k k,那么不足的高位看做是 0 0 0
  2. 如果循环长度是 L L L,那么说明对于任意的正整数 a a a n n n a a a 次幂和 a + L a+L a+L 次幂的最后 k k k 位都相同。

输入格式

共一行,包含两个整数 n n n k k k n n n k k k 之间用一个空格隔开,表示要求 n n n 的正整数次幂的最后 k k k 位的循环长度。

输出格式

一个整数,表示循环长度。如果循环不存在,输出 − 1 -1 1
样例 #1

样例输入 #1

32 2

样例输出 #1

4

提示

【数据范围】

对于 30 % 30 \% 30% 的数据,满足 k ≤ 4 k \le 4 k4
对于 100 % 100 \% 100% 的数据,满足 1 ≤ n < 10 100 1 \le n < {10}^{100} 1n<10100 1 ≤ k ≤ 100 1 \le k \le 100 1k100


代码实现

#include <iostream>
#include <vector>
using namespace std;

class BigInt : public vector<int> {
public :
    BigInt() { push_back(0); }
    BigInt(int n, int v) : vector<int>(n, v) {}
    BigInt(int x) {
        push_back(x);
        proccess_digit();
        return ;
    }
    BigInt(string &s, int k) {
        for (int i = 0, j = s.size() - 1; i < k; i++, j--) {
            push_back(s[j] - '0');
        }
        return ;
    }
    BigInt &operator *=(int x) {
        for (int i = 0; i < size(); i++) at(i) *= x;
        proccess_digit();
        return *this;
    }
    BigInt operator*(const BigInt &a) {
        BigInt ret(min(MaxLen, int(size() + a.size() - 1)), 0);
        for (int i = 0; i < size(); i++) {
            for (int j = 0; j < a.size(); j++) {
                if (i + j >= MaxLen) continue;
                ret[i + j] += at(i) * a[j];
            }
        }
        ret.proccess_digit();
        return ret;
    }

    static int MaxLen;

private:
    void proccess_digit() {
        for (int i = 0; i < size(); i++) {
            if (at(i) < 10) continue;
            if (i + 1 < MaxLen) {
                if (i + 1 == size()) push_back(0);
                at(i + 1) += at(i) / 10;
            }
            at(i) %= 10;
        }
        return ;
    }
};

int BigInt::MaxLen = 0;

ostream &operator<<(ostream &out, const BigInt &a) {
    for (int i = a.size() - 1; i >= 0; --i) {
        out << a[i];
    }
    return out;
}

int main() {
    string s;
    int k;
    cin >> s >> k;
    BigInt::MaxLen = k;
    BigInt n(s, k);
    BigInt pre_y = n, y;
    vector<int> arr;
    for (int i = 0; i < n.size(); i++) {
        y = pre_y;
        int cnt = 1;
        while ((y * n).at(i) != n[i]) {
            y = y * pre_y;
            cnt += 1;
            if (cnt == 11) break;
        }
        if (cnt == 11) {
            cout << "-1" << endl;
            return 0;
        }
        arr.push_back(cnt);
        pre_y = y;
    }
    BigInt ans = 1;
    for (int i = 0; i < arr.size(); i++) {
        ans *= arr[i];
    }
    cout << ans << endl;
    return 0;
}


10.传球游戏

题目描述

上体育课的时候,小蛮的老师经常带着同学们一起做游戏。这次,老师带着同学们一起做传球游戏。

游戏规则是这样的: n n n 个同学站成一个圆圈,其中的一个同学手里拿着一个球,当老师吹哨子时开始传球,每个同学可以把球传给自己左右的两个同学中的一个(左右任意),当老师再次吹哨子时,传球停止,此时,拿着球没有传出去的那个同学就是败者,要给大家表演一个节目。

聪明的小蛮提出一个有趣的问题:有多少种不同的传球方法可以使得从小蛮手里开始传的球,传了 m m m 次以后,又回到小蛮手里。两种传球方法被视作不同的方法,当且仅当这两种方法中,接到球的同学按接球顺序组成的序列是不同的。比如有三个同学 1 1 1 号、 2 2 2 号、 3 3 3 号,并假设小蛮为 1 1 1 号,球传了 3 3 3 次回到小蛮手里的方式有 1 → 2 → 3 → 1 1 \rightarrow 2 \rightarrow 3 \rightarrow 1 1231 1 → 3 → 2 → 1 1 \rightarrow 3 \rightarrow 2 \rightarrow 1 1321,共 2 2 2 种。

输入格式

一行,有两个用空格隔开的整数 n , m ( 3 ≤ n ≤ 30 , 1 ≤ m ≤ 30 ) n,m(3 \le n \le 30,1 \le m \le 30) n,m(3n30,1m30)

输出格式

1 1 1 个整数,表示符合题意的方法数。

样例 #1

样例输入 #1

3 3

样例输出 #1

2

提示

数据范围及约定

  • 对于 40 % 40\% 40% 的数据,满足: 3 ≤ n ≤ 30 , 1 ≤ m ≤ 20 3 \le n \le 30,1 \le m \le 20 3n30,1m20
  • 对于 100 % 100\% 100% 的数据,满足: 3 ≤ n ≤ 30 , 1 ≤ m ≤ 30 3 \le n \le 30,1 \le m \le 30 3n30,1m30

递推状态: dp[ i ][ j ]:第i次到达j的方法总数。
递推公式:
dp[ i ][ j ]=dp[ i-1 ][ j-1 ]+dp[ i-1 ][ j+1] (1<j<n)
dp[ i ][ 1 ]=dp[ i-1 ][ 2 ]+dp[ i-1 ][ n ]
dp[ i ][ n ]=dp[ i-1 ][ 1 ]+dp[ i-1 ][ n-1 ]
边界条件: dp[ 0 ][ 1 ]=1
代码实现:

#include<iostream>
using namespace std;
#define MAXSIZE 30
int dp[MAXSIZE+5][MAXSIZE+5]={0};
int main()
{
	int n,m;
	cin>>n>>m;
	dp[0][1]=1;
	for(int i=1;i<=m;i++)
	{
		for(int j=2;j<n;j++)
		{
			dp[i][j]=dp[i-1][j+1]+dp[i-1][j-1];
		}
		dp[i][1]=dp[i-1][2]+dp[i-1][n];
		dp[i][n]=dp[i-1][1]+dp[i-1][n-1];
	}
	cout<<dp[m][1];
	return 0;
}


11.Honoi双塔问题

题目描述

给定 A、B、C 三根足够长的细柱,在 A 柱上放有 2 n 2n 2n 个中间有孔的圆盘,共有 n n n 个不同的尺寸,每个尺寸都有两个相同的圆盘,注意这两个圆盘是不加区分的(下图为 n = 3 n=3 n=3 的情形)。

现要将这些圆盘移到 C 柱上,在移动过程中可放在 B 柱上暂存。要求:

  1. 每次只能移动一个圆盘;
  2. A、B、C 三根细柱上的圆盘都要保持上小下大的顺序。

任务:设 A n A_n An 2 n 2n 2n 个圆盘完成上述任务所需的最少移动次数,对于输入的 n n n,输出 A n A_n An

输入格式

一个正整数 n n n,表示在 A 柱上放有 2 n 2n 2n 个圆盘。

输出格式

一个正整数, 为完成上述任务所需的最少移动次数 A n A_n An

样例 #1

样例输入 #1

1

样例输出 #1

2

样例 #2

样例输入 #2

2

样例输出 #2

6

提示

限制

  • 对于 50 % 50\% 50% 的数据, 1 ≤ n ≤ 25 1 \le n \le 25 1n25
  • 对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 200 1 \le n \le 200 1n200

提示

设法建立 A n A_n An A n − 1 A_{n-1} An1 的递推关系式。


递推状态: dp[ i ]:n=i时的交换总次数。
递推公式: dp[ i ]=dp[ i-1 ]*2+2。
将2i个圆盘转移到C的次数为将2(i-1)个圆盘转移到B,将2个圆盘转移到C,再把2(i-1)个圆盘转移到C的次数和。
边界条件: dp[ 1 ]=2。
代码实现:

#include<iostream>
#include<vector>
using namespace std;
#define MAX_N 200
class BigInt:public vector<int>{
	public:
		BigInt(){push_back(0);}
		BigInt(int x)
		{
			push_back(x);
			proccess_digit();
		}
		BigInt operator+=(int x)
		{
			at(0)+=x;
			proccess_digit();
			return *this;
		}
		BigInt operator+(int x)
		{
			BigInt ret(*this);
			ret+=x;
			return ret;
		}
		BigInt operator*=(int x)
		{
			for(int i=0;i<size();i++)
			{	
				at(i)*=x;
			}
			proccess_digit();
			return *this;
		}
		BigInt operator*(int x)
		{
			BigInt ret(*this);
			ret*=x;
			return ret;
		}
		void proccess_digit()
		{
			for(int i=0;i<size();i++)
			{
				if(at(i)<10)continue;
				if(i==size()-1)push_back(0);
				at(i+1)+=at(i)/10;
				at(i)%=10;
			}
			return ;
		}
};
ostream &operator<<(ostream &out, const BigInt &a) {
    for (int i = a.size() - 1; i >= 0; i--) {
        out << a[i];
    }
    return out;
}
BigInt dp[MAX_N+5];
int main()
{
	int n;
	cin>>n;
	dp[1]=2;
	for(int i=2;i<=n;i++)
	{
		dp[i]=dp[i-1]*2+2;
	}
	cout<<dp[n];
	return 0;
}

动态规划

1.动态规划求解的一般过程

1、确定动归状态
例如:f(i,j) 代表从底边走到 (i,j) 点所能获得的最大值

2、确定状态转移方程
在这里插入图片描述

3、正确性证明:求助于数学归纳法

4、程序实现(递归+记忆化或循环)

2.重要概念

在这里插入图片描述

3.例题

1.数字三角形

题目描述
有一个由数字组成的三角形数塔,站在上一层的某个点,只能到达其下方左右的两个点。现在请找到一条从上到下的路径,使得路径上所有数字相加之和最大

4274F9B8E6E14581A4EC851BCE349029.jpg

输入
第一行输入一个数字 n(1≤n≤1000)代表数塔层数

接下来n行,按数塔图形,每行有一个或多个的整数,表示该层节点的值(节点值≤100000)
输出
输出一个整数,代表从上到下路径上所有数字相加和的最大值。

本题 BUG 已解决!

样例输入1

6
3
9 5
4 2 1
3 4 9 6
3 5 3 7 3
2 1 3 9 3 2

样例输出1

39


动归状态 dp[ i ][ j ]:从最底层到达第i行第j个位置的最大值
动归方程 dp[ i ][ j ]=max(dp[ i+1 ][ j ]+dp[ i+1 ][ j+1 ])+val[ i ]
代码实现

#include<iostream>
using namespace std;
#define MAXSIZE 1000
int dp[MAXSIZE+5][MAXSIZE+5]={0};
int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=i;j++)
		{
			cin>>dp[i][j];
		}
	}
	for(int i=n;i>=1;i--)
	{
		for(int j=1;j<=i;j++)
		{
			dp[i][j]+=max(dp[i+1][j],dp[i+1][j+1]);	
		}
	}
	cout<<dp[1][1];
	return 0;
}

在这里插入图片描述



2.0/1背包

题目描述
给一个能承重V的背包,和n件物品,我们用重量和价值的二元组来表示一个物品,第i件物品表示为(Vi,Wi),问:在背包不超重的情况下,得到物品的最大价值是多少?
在这里插入图片描述

输入
第一行输入两个数 V,n,分别代表背包的最大承重和物品数。

接下来n行,每行两个数Vi,Wi,分别代表第i件物品的重量和价值。

(Vi≤V≤10000,n≤100,Wi≤1000000)
输出
输出一个整数,代表在背包不超重情况下所装物品的最大价值。

样例输入1

15 4
4 10
3 7
12 12
9 8

样例输出1

19


动归状态 dp[ i ][ j ]:前i件物品在背包承重为j的情况下的最大价值。
动归方程 dp[ i ][ j ]=max(dp[ i-1 ][ j-v[ i ] ]+w[ i ],dp[ i-1 ][ j ])。
代码实现1(基础版本)

#include<iostream>
using namespace std;
#define MAX_V 10000
#define MAX_N 100
int dp[MAX_N+5][MAX_V+5]={0};
int main()
{
	int V,n;
	cin>>V>>n;
	for(int i=1,vi,wi;i<=n;i++)
	{
		cin>>vi>>wi;
		for(int j=1;j<=V;j++)
		{
			dp[i][j]=dp[i-1][j];
			if(j>=vi)dp[i][j]=max(dp[i][j],dp[i-1][j-vi]+wi);
		}
	}
	cout<<dp[n][V];
	return 0;
 } 

代码实现2(滚动数组)

#include<iostream>
using namespace std;
#define MAX_V 10000
#define MAX_N 100
int dp[2][MAX_V+5]={0};
int main()
{
	int V,n;
	cin>>V>>n;
	for(int i=1,vi,wi;i<=n;i++)
	{
		cin>>vi>>wi;
		for(int j=1;j<=V;j++) 
		{
			dp[i%2][j]=dp[1-i%2][j];
			if(j>=vi)dp[i%2][j]=max(dp[i%2][j],dp[1-i%2][j-vi]+wi);
		}
	}
	cout<<dp[n%2][V];
	return 0;
 } 

代码实现3(一维数组)

#include<iostream>
using namespace std;
#define MAX_V 10000
#define MAX_N 100
int dp[MAX_V+5]={0};
int main()
{
	int V,n;
	cin>>V>>n;
	for(int i=1,vi,wi;i<=n;i++)
	{
		cin>>vi>>wi;
		for(int j=V;j>=vi;j--)//注意这里要从后向前 
		{
			dp[j]=max(dp[j],dp[j-vi]+wi);
		}
	}
	cout<<dp[V];
	return 0;
 } 


3.完全背包

题目描述
有N种物品和一个容量为 V 的背包,每种物品都有无限件可用。

第 i 种物品的体积是Ci,价值是Wi。求解在不超过背包容量的情况下,能够获得的最大价值。

54E9C51263E1462585A8F6595841EEC0.jpg

输入
第一行为两个整数N、V(1≤N,V≤10000),分别代表题目描述中的物品种类数量N和背包容量V。

后跟N行,第 i 行两个整数Ci、Vi,分别代表每种物品的体积和价值。

输出
输出一个整数,代表可获得的最大价值。

样例输入

5 20
2 3
3 4
10 9
5 2
11 11

样例输出

30


动归状态 dp[ i ][ j ]:前i种物品在背包承重为j的情况下的最大价值。
动归方程 dp[ i ][ j ]=max(dp[ i ][ j-v[ i ] ]+w[ i ],dp[ i-1 ][ j ])。
代码实现

#include<iostream>
using namespace std;
#define MAXSIZE 10000
int dp[MAXSIZE+5]={0};
int main()
{
	int N,V;
	cin>>N>>V;
	for(int i=1,ci,wi;i<=N;i++)
	{
		cin>>ci>>wi;
		for(int j=ci;j<=V;j++)//注意此时正向刷表,和0/1背包区分 
		{
			dp[j]=max(dp[j],dp[j-ci]+wi);
		}
	}
	cout<<dp[V];
	return 0;
}


4.多重背包

题目描述
给有一个能承重 V 的背包,和n种物品,每种物品的数量有限多,我们用重量、价值和数量的三元组来表示一个物品,第 i 件物品表示为(Vi,Wi,Si),问在背包不超重的情况下,得到物品的最大价值是多少?

54E9C51263E1462585A8F6595841EEC0.jpg

输入
第一行输入两个数V、n,分别代表背包的最大承重和物品种类数。

接下来 n 行,每行三个数 Vi、Wi、Si,分别代表第 i 种物品的重量、价值和数量。

输出
输出一个整数,代表在背包不超重情况下所装物品的最大价值。

样例输入1

15 4
4 10 5
3 7 4
12 12 2
9 8 7

样例输出1

37


思路: 把该问题转化为0/1背包问题去做。(不是最优,后面会有优化)

#include<iostream>
using namespace std;
#define MAX_V 100000
#define MAX_S 100000
#define MAX_N 100
int dp[MAX_V+5]={0};
int main()
{
	int V,N;
	cin>>V>>N;
	for(int i=1,vi,wi,si;i<=N;i++)
	{
		cin>>vi>>wi>>si;
		for(int j=1;j<=si;j++)
		{
			for(int k=V;k>=vi;k--)
			{
				dp[k]=max(dp[k],dp[k-vi]+wi);
			}
		}
	}
	cout<<dp[V];
	return 0;
} 


5.最长上升子序列

题目描述
有一个数字序列,求其中最长严格上升子序列的长度

B28E0055CAC24D4D8F260936BD6F358F.jpg

输入
输入一个数字n (1≤n≤1000000),代表数字序列的长度。

后跟 n​ 个整数,第 i​ 个整数 ai(1≤ai≤10000)​,代表数字序列中的第 i​ 个值。

输出
输出一个整数,代表所求的最长严格上升子序列的长度。

样例输入

10
3 2 5 7 4 5 7 9 6 8

样例输出

5


动归状态 dp[ i ]:以i结尾的上升子序列的最大长度。
动归方程 dp[ i ]=max(dp[ j ]+1)(0<j<i且arr[ j ]<arr[ i ])。
代码实现(非最优实现)

#include<iostream>
using namespace std;
#define MAX_N 1000000
int dp[MAX_N]={0};
int arr[MAX_N];
int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&arr[i]);
	}
	int ans=0;
	for(int i=1;i<=n;i++)
	{
		dp[i]=1;
		for(int j=0;j<i;j++)
		{
			if(arr[j]<arr[i])dp[i]=max(dp[i],dp[j]+1);
		}
		ans=max(ans,dp[i]);
	}
	cout<<ans;
	return 0;
 } 


6.最长公共子序列

题目描述
给出两个字符串,求其两个的最长公共子序列长度。

输入
第一行输入一个字符串s1,第二行输入一个字符串s2 (字符串长度≤1000) ,两个字符串长度可以不相同。

输出
输出一个整数,代表两个字符串的最长公共子序列的长度。

样例输入1

sehuaizexi
yhaizeyiux

样例输出1

6

动归状态 dp[ i ][ j ]:s1的前i位和s2的前j位的最长公共子序列。
动归方程 dp[ i ]=max(dp[ i-1 ][ j ],dp[ i ][ j-1 ]) (s1[ i ]!=s2[ j ])
dp[ i ][ j ]=dp[ i-1 ][ j-1 ]+1(s1[ i ]=s2[ j ])
代码实现

#include<iostream>
#include<cstring>
using namespace std;
#define MAX_N 1000
int dp[MAX_N+5][MAX_N+5]={0};
char s1[MAX_N+5],s2[MAX_N+5];
int main()
{
	cin>>(s1+1);
	cin>>(s2+1);
	int len1=strlen(s1+1),len2=strlen(s2+1);
	for(int i=1;i<=len1;i++)
	{
		for(int j=1;j<=len2;j++)
		{
		if(s1[i]==s2[j])dp[i][j]=dp[i-1][j-1]+1;
		else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
		}
	}
	cout<<dp[len1][len2];
	return 0;
}


7.切割回文

题目描述
给出一个字符串S,问对字符串S最少切几刀,使得分成的每一部分都是一个回文串(注意:单一字符是回文串)

87699633.jpg

输入
一个长度为n(1≤n≤500000)​的字符串S,只包含小写字母。

输出
输出一个整数,代表所切的最少刀数。

样例输入

sehuhzzexe

样例输出

4


动归状态 dp[ i ][ j ]:在区间[i,j]内的最少切割次数。
动归方程
在这里插入图片描述
本题为
代码实现(非最优实现,区间dp)

#include<iostream>
#include<cstring>
#include<cinttypes>
using namespace std;
#define MAX_N 5000
int dp[MAX_N+5][MAX_N+5];
char arr[MAX_N+5];
int main()
{
	scanf("%s",arr+1);
	int n=strlen(arr+1);
	for(int l=1;l<=n;l++)
	{
		for(int i=1;i<=n-l+1;i++)
		{
			int j=i+l-1;
			if(arr[i]==arr[j]&&dp[i+1][j-1]==0)dp[i][j]=0;
			else
			{
				dp[i][j]=l;
				for(int k=i;k<j;k++)
				{
					dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+1);
				}
			}
		}		
	}
	cout<<dp[1][n];
	return 0;
 } 


8.棋盘分割

题目描述
​ 将一个8*8的棋盘进行如下分割:将原棋盘割下一块矩形棋盘并使剩下部分也是矩形,再将剩下的部分继续如此分割,这样割了(n−1)次后,连同最后剩下的矩形棋盘共有 n 块矩形棋盘。(每次切割都只能沿着棋盘格子的边进行)

在这里插入图片描述

​ 原棋盘上每一格有一个分值,一块矩形棋盘的总分为其所含各格分值之和。现在需要把棋盘按上述规则分割n块矩形棋盘,并使各矩形棋盘总分的平方和最小。

​ 请编程对给出的棋盘及 n,求出平方和的最小值。

输入
​ 第1行为一个整数n(1<n<15)。

​ 第2行至第9行每行为8个小于100的非负整数,表示棋盘上相应格子的分值。每行相邻两数之间用一个空格分隔。

输出
​ 仅一个数,为最小的平方和值。

输入样例1

3
1 1 1 1 1 1 1 3
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 0
1 1 1 1 1 1 0 3

输出样例1

1460


本题为二维区间dp问题
动归状态 dp[ t ][ i ][ j ][[ k ][ l ]:在(i,j)-(k,l)范围内分割y次的情况下的最小结果。
动归方程
在这里插入图片描述

在这里插入图片描述

代码实现

#include<iostream>
using namespace std;
int arr[10][10]={0};
int dp[20][10][10][10][10]={0};
int VAL(int i,int j,int k,int l)
{
	return arr[k][l]-arr[k][j-1]-arr[i-1][l]+arr[i-1][j-1];
}
int S(int x)
{
	return x*x;
}
int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=8;i++)//求二位前缀和 
	{
		for(int j=1;j<=8;j++)
		{
			cin>>arr[i][j];
			arr[i][j]+=(arr[i-1][j]+arr[i][j-1]-arr[i-1][j-1]);
		}
	}
	for(int i=1;i<=8;i++)//边界条件,t=1的情况 
	{
		for(int j=1;j<=8;j++)
		{
			for(int k=i;k<=8;k++)
			{
				for(int l=j;l<=8;l++)
				{
					dp[1][i][j][k][l]=S(VAL(i,j,k,l));		
				}
			}
		}
	}
	for(int t=2;t<=n;t++)
	{
	for(int i=1;i<=8;i++)
	{
		for(int j=1;j<=8;j++)
		{
			for(int k=i;k<=8;k++)
			{
				for(int l=j;l<=8;l++)
				{
					int ans=0x3f3f3f3f;
					for(int u=i;u<k;u++)
					{
						int val1=dp[t-1][i][j][u][l]+dp[1][u+1][j][k][l];
						int val2=dp[t-1][u+1][j][k][l]+dp[1][i][j][u][l];
						ans=min(ans,min(val1,val2));
					} 
					for(int v=j;v<l;v++)
					{
						int val3=dp[t-1][i][j][k][v]+dp[1][i][v+1][k][l];
						int val4=dp[t-1][i][v+1][k][l]+dp[1][i][j][k][v];
						ans=min(ans,min(val3,val4));
					}
					dp[t][i][j][k][l]=ans;
				}
			}
		}
	}
	}
	cout<<dp[n][1][1][8][8];
	return 0;
}


9.windy数

题目背景

windy 定义了一种 windy 数。

题目描述

不含前导零且相邻两个数字之差至少为 2 2 2 的正整数被称为 windy 数。windy 想知道,在 a a a b b b 之间,包括 a a a b b b ,总共有多少个 windy 数?

输入格式

输入只有一行两个整数,分别表示 a a a b b b

输出格式

输出一行一个整数表示答案。

样例 #1

样例输入 #1

1 10

样例输出 #1

9

样例 #2

样例输入 #2

25 50

样例输出 #2

20

数据规模与约定

对于全部的测试点,保证 1 ≤ a ≤ b ≤ 2 × 1 0 9 1 \leq a \leq b \leq 2 \times 10^9 1ab2×109


思路:本题为数位dp问题,这类问题的特征是给定区间[a,b],求该范围内的符合条件的数有多少个,数据范围比较大,但可以把一位一位数遍历的问题转化为对位数的遍历,这类问题一般有固定的模板。


代码实现

#include<iostream>
#include<cstring>
using namespace std;
int dp[15][15];
int arr[15];
int len=0;
/*pos是当前搜索到的位置,
pre为前一位的数,记录pre的作用是为了判断相差是否小于2,
limit的作用是指示前面是否出现了每一位数都紧贴上界的情况,作用是方便选择当前一位数的取值范围,
zero记录了是否出现前导零的情况 */ 
long long dfs(int pos,int pre,int limit,int zero)
{
	if(pos>len)return 1;
	if(!limit&&dp[pos][pre]!=-1)return dp[pos][pre];//若当前位数没有限制,且已经进行了缓存,则直接return 
	int up=limit?arr[len-pos+1]:9;//根据是否限制来去pos位的最大值 
	long long ret=0;
	for(int i=0;i<=up;i++) 
	{
		if(abs(pre-i)<2)continue;
		if(zero&&i==0)ret+=dfs(pos+1,-2,0,1);//若为前导零 
		else ret+=dfs(pos+1,i,limit&&i==up,0);//不为前导零 
	}
	if(!limit)dp[pos][pre]=ret;//对第一次出现的该情况进行缓存 
	return ret;
}
int solve(long long x)
{
	len=0;
	while(x)arr[++len]=x%10,x/=10;//把每一位取出放到arr上 
	memset(dp,-1,sizeof(dp));
	return dfs(1,-2,1,1);//-2的作用是保证不会出现相差<2的情况 
}
int main()
{
	long long a,b;
	cin>>a>>b;
	//[a,b]范围内的取值等价于[1,b]的取值减去[1,a-1]的取值。 
	long long ret1=solve(a-1);
	long long ret2=solve(b);
	cout<<ret2-ret1<<endl;
	return 0;
 } 


10.Round_Numbers

题目描述
​ Round Numbers 就是一个表示成二进制的时候0比1多或者相等的正数,注意是正数,所以0就肯定不是了。

​ 题目是给定一个区间,问在这个区间上的Round Numbers有多少个?

输入
​ 输入文件包含多个测试数据。

​ 每组测试数据占一行,含有两个整数 L
�
和 R(1≤L,R≤231−1)

​ 输入文件以 EOF 结束。

输出
​ 对于每组测试数据,在单独的一行内输出结果。

输入样例1

2 12

输出样例1

6

数据规模与限定
时间限制:1 s

内存限制:64 M


代码实现

#include<iostream>
#include<cstring>
using namespace std;
int dp[35][35][35];
int arr[35];
int len=0;
long long dfs(int pos,int num0,int num1,int limit,int zero)
{
	if(pos>len&&num0-num1>=0)return 1;
	if(pos>len&&num0-num1<0)return 0;
	if(!limit&&dp[pos][num0][num1]!=-1)return dp[pos][num0][num1];
	int up=limit?arr[len-pos+1]:1;
	long long ret=0;
	for(int i=0;i<=up;i++)
	{
		if(zero&&i==0)ret+=dfs(pos+1,0,0,0,1);
		else ret+=dfs(pos+1,num0+(i==0),num1+(i==1),limit&&i==up,0);
	}
	if(!limit)dp[pos][num0][num1]=ret;
	return ret;
}
long long solve(long long x)
{
	len=0;
	memset(dp,-1,sizeof(dp));
	while(x)arr[++len]=x%2,x/=2;
	return dfs(1,0,0,1,1);
}
int main()
{
	long long L,R;
	cin>>L>>R;
	long long ret1=solve(L-1);
	long long ret2=solve(R);
	cout<<ret2-ret1;
	return 0;
 } 


11.没有上司的舞会

题目描述

某大学有 n n n 个职员,编号为 1 … n 1\ldots n 1n

他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。

现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 r i r_i ri,但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。

所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。

输入格式

输入的第一行是一个整数 n n n

2 2 2 到第 ( n + 1 ) (n + 1) (n+1) 行,每行一个整数,第 ( i + 1 ) (i+1) (i+1) 行的整数表示 i i i 号职员的快乐指数 r i r_i ri

( n + 2 ) (n + 2) (n+2) 到第 2 n 2n 2n 行,每行输入一对整数 l , k l, k l,k,代表 k k k l l l 的直接上司。

输出格式

输出一行一个整数代表最大的快乐指数。

样例 #1


7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5

样例输出 #1

5

提示

数据规模与约定

对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 6 × 1 0 3 1\leq n \leq 6 \times 10^3 1n6×103 − 128 ≤ r i ≤ 127 -128 \leq r_i\leq 127 128ri127 1 ≤ l , k ≤ n 1 \leq l, k \leq n 1l,kn,且给出的关系一定是一棵树。


思路:本题为树形dp问题,状态转移方程f[ i ]和g[ i ]分别表示以i为根节点选择和不选择i的i的最大开心指数。

代码实现:

#include<iostream>
#include<vector>
using namespace std;
vector<int>G[600005];
int happy[600005];
bool child[600005]={0};
int f[600005],g[600005];
void dfs(int k)
{
	for(auto i:G[k])
	{
		dfs(i);
		f[k]+=g[i];
		g[k]+=max(f[i],g[i]);
	}
	return ;
}
int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>happy[i];
		f[i]=happy[i];
		g[i]=0;	
	}
	for(int i=1,u,v;i<=n-1;i++)
	{
		cin>>u>>v;
		G[v].push_back(u);
		child[u]=1;
	}
	int root=0;
	for(int i=1;i<=n;i++)
	{
		if(child[i]==0)
		{
			root=i;
			break;
		}
	}
	dfs(root);
	cout<<max(f[root],g[root]);
	return 0;
}


动归优化

1.去除冗余状态

1.乌龟棋

题目描述

小明过生日的时候,爸爸送给他一副乌龟棋当作礼物。

乌龟棋的棋盘是一行 N N N 个格子,每个格子上一个分数(非负整数)。棋盘第 1 1 1 格是唯一的起点,第 N N N 格是终点,游戏要求玩家控制一个乌龟棋子从起点出发走到终点。

乌龟棋中 M M M 张爬行卡片,分成 4 4 4 种不同的类型( M M M 张卡片中不一定包含所有 4 4 4 种类型的卡片,见样例),每种类型的卡片上分别标有 1 , 2 , 3 , 4 1,2,3,4 1,2,3,4 四个数字之一,表示使用这种卡片后,乌龟棋子将向前爬行相应的格子数。游戏中,玩家每次需要从所有的爬行卡片中选择一张之前没有使用过的爬行卡片,控制乌龟棋子前进相应的格子数,每张卡片只能使用一次。

游戏中,乌龟棋子自动获得起点格子的分数,并且在后续的爬行中每到达一个格子,就得到该格子相应的分数。玩家最终游戏得分就是乌龟棋子从起点到终点过程中到过的所有格子的分数总和。

很明显,用不同的爬行卡片使用顺序会使得最终游戏的得分不同,小明想要找到一种卡片使用顺序使得最终游戏得分最多。

现在,告诉你棋盘上每个格子的分数和所有的爬行卡片,你能告诉小明,他最多能得到多少分吗?

输入格式

每行中两个数之间用一个空格隔开。

1 1 1 2 2 2 个正整数 N , M N,M N,M,分别表示棋盘格子数和爬行卡片数。

2 2 2 N N N 个非负整数, a 1 , a 2 , … , a N a_1,a_2,…,a_N a1,a2,,aN,其中 a i a_i ai 表示棋盘第 i i i 个格子上的分数。

3 3 3 M M M 个整数, b 1 , b 2 , … , b M b_1,b_2,…,b_M b1,b2,,bM,表示 M M M 张爬行卡片上的数字。

输入数据保证到达终点时刚好用光 M M M 张爬行卡片。

输出格式

一个整数,表示小明最多能得到的分数。

样例 #1

样例输入 #1

9 5
6 10 14 2 8 8 18 5 17
1 3 1 2 1

样例输出 #1

73

提示

每个测试点 1s。

小明使用爬行卡片顺序为 1 , 1 , 3 , 1 , 2 1,1,3,1,2 1,1,3,1,2,得到的分数为 6 + 10 + 14 + 8 + 18 + 17 = 73 6+10+14+8+18+17=73 6+10+14+8+18+17=73。注意,由于起点是 1 1 1,所以自动获得第 1 1 1 格的分数 6 6 6

对于 30 % 30\% 30% 的数据有 1 ≤ N ≤ 30 , 1 ≤ M ≤ 12 1≤N≤30,1≤M≤12 1N30,1M12

对于 50 % 50\% 50% 的数据有 1 ≤ N ≤ 120 , 1 ≤ M ≤ 50 1≤N≤120,1≤M≤50 1N120,1M50,且 4 4 4 种爬行卡片,每种卡片的张数不会超过 20 20 20

对于 100 % 100\% 100% 的数据有 1 ≤ N ≤ 350 , 1 ≤ M ≤ 120 1≤N≤350,1≤M≤120 1N350,1M120,且 4 4 4 种爬行卡片,每种卡片的张数不会超过 40 40 40 0 ≤ a i ≤ 100 , 1 ≤ i ≤ N , 1 ≤ b i ≤ 4 , 1 ≤ i ≤ M 0≤a_i≤100,1≤i≤N,1≤b_i≤4,1≤i≤M 0ai100,1iN,1bi4,1iM


动归状态 dp[ i ][ j ][ k ][ l ]:在卡片1使用i,卡片2使用j张,卡片3使用k张,卡片4使用l张的情况下所能取得的最大收益。
动归方程
dp[ i ][ j ][ k ][ l ]=max(dp[ i-1 ][ j ][ k ][ l ],dp[ i ][ j-1 ][ k ][ l ],dp[ i ][ j ][ k-1 ][ l ],dp[ i ][ j ][ k ][ l-1 ])+val[ s ] (s=a+2b+3c+4d)
代码实现
去除冗余状态之前

#include<iostream>
using namespace std;
#define MAX_N 350
#define MAX_M 120
int a[MAX_N+5];
int kind[5]={0};
int dp[50][50][50][50]={0};
int main()
{
	int N,M;
	cin>>N>>M;
	for(int i=0;i<N;i++)
	cin>>a[i];
	for(int i=1,x;i<=M;i++)
	{
		cin>>x;
		kind[x]++;	
	}
	dp[0][0][0][0]=a[0];
	for(int i=0;i<=kind[1];i++)
	{
		for(int j=0;j<=kind[2];j++)
		{
			for(int k=0;k<=kind[3];k++)
			{
				for(int l=0;l<=kind[4];l++)
				{
					int ans=0;
					int s=i+2*j+3*k+4*l;
					if(i)ans=max(ans,dp[i-1][j][k][l]);
					if(j)ans=max(ans,dp[i][j-1][k][l]);
					if(k)ans=max(ans,dp[i][j][k-1][l]);
					if(l)ans=max(ans,dp[i][j][k][l-1]);
					dp[i][j][k][l]=ans+a[s];
				}
			}
		}
	}
	cout<<dp[kind[1]][kind[2]][kind[3]][kind[4]];
	return 0;
 } 

去除冗余状态之后

#include<iostream>
using namespace std;
#define MAX_N 350
#define MAX_M 120
int a[MAX_N+5];
int kind[5]={0};
int dp[50][50][50]={0};
int main()
{
	int N,M;
	cin>>N>>M;
	for(int i=0;i<N;i++)
	cin>>a[i];
	for(int i=1,x;i<=M;i++)
	{
		cin>>x;
		kind[x]++;	
	}
	dp[0][0][0]=a[0];
	for(int i=0;i<=kind[1];i++)
	{
		for(int j=0;j<=kind[2];j++)
		{
			for(int k=0;k<=kind[3];k++)
			{
				for(int l=0;l<=kind[4];l++)
				{
					int ans=0;
					int s=i+2*j+3*k+4*l;
					if(i)ans=max(ans,dp[j][k][l]);
					if(j)ans=max(ans,dp[j-1][k][l]);
					if(k)ans=max(ans,dp[j][k-1][l]);
					if(l)ans=max(ans,dp[j][k][l-1]);
					dp[j][k][l]=ans+a[s];
				}
			}
		}
	}
	cout<<dp[kind[2]][kind[3]][kind[4]];
	return 0;
 } 


2.状态重定义

1.墙壁涂色

题目描述
给一个环形的墙壁涂颜色,颜色一共有 k 种,墙壁被竖直地划分成 n 个部分,相邻的部分颜色不能相同。请你写程序计算出一共有多少种给墙壁上色的方案?

例如,当 n=5,k=3​ 时,下面是一种合法的涂色方案

在这里插入图片描述

而由于墙壁是环形的,所以下面就是一种非法的方案

在这里插入图片描述

输入
输入两个数字 n,k(1≤n≤103,2≤k≤10),分别代表墙壁数量和颜色种类。

输出
对于每个询问,输出一行整数,合法的墙壁涂色方案数。

样例输入1

5 3

样例输出1

30


代码实现
原先版本

#include<iostream>
#include<vector>
using namespace std;
#define MAX_N 1000
#define MAX_K 10
//本题使用大整数版本来实现 
class BigInt:public vector<int>
{
	public:
		BigInt(){push_back(0);}
		BigInt(int x){
			push_back(x);
			proccess_digit();
		}
		BigInt &operator+=(const BigInt&a)
		{
			for(int i=0;i<a.size();i++)
			{
				if(i>=size())push_back(a[i]);
				else at(i)+=a[i];
			}
			proccess_digit();
			return*this;
		}
		BigInt operator+(const BigInt&a)
		{
			BigInt ret(*this);
			ret+=a;
			return ret;
		}
		void proccess_digit()
		{
			for(int i=0;i<size();i++)
			{
				if(at(i)<100000)continue;
				if(i==size()-1)push_back(0);
				at(i+1)+=at(i)/100000;
				at(i)%=100000;
			}
			return ;
		}
};
ostream&operator<<(ostream &out,BigInt &a)
{
	out<<a[a.size()-1]; 
	for(int i=a.size()-2;i>=0;i--)
	{
		for(int j=10000;j>0;j/=10)
		{
			out<<a[i]%(j*10)/j;
		}
	}
	return out;
}
BigInt dp[2][MAX_K+5][MAX_K+5]={0};//滚动数组避免内存超限 
int main()
{
	int n,k;
	cin>>n>>k;
	for(int i=1;i<=k;i++)dp[1][i][i]=1;
	for(int i=1;i<=k;i++)
	{
		for(int j=1;j<=k;j++)
		{
			if(i!=j)dp[0][i][j]=1;
		}
	}
	for(int s=3;s<=n;s++)
	{
		for(int i=1;i<=k;i++)
		{
			for(int j=1;j<=k;j++)
			{
				dp[s%2][i][j]=0;
				for(int l=1;l<=k;l++)
				{	
					if(l==j)continue;
					dp[s%2][i][j]+=dp[(s-1)%2][i][l];
				}
			}
		}
	}
	BigInt ans=0;
	for(int i=1;i<=k;i++)
	{
		for(int j=1;j<=k;j++)
		{
			if(i==j)continue;
			ans+=dp[n%2][i][j]; 
		}
	}
	cout<<ans;
	return 0;
}

优化版本
动归状态 dp[ i ]:前i个位置的最大合法方案数量。
动归方程
dp[ i ]=(k-1)*dp[ i-2 ]+(k-2)*dp[ i-2 ]
前i个位置的最大合法方案数量为第i-1个位置与开头相同的数量以及第i-1个位置与开头不同的数量之和。

初始条件
dp[ 1 ]=k,dp[ 2 ]=k*(k-1),dp[ 3 ]=k*(k-1)*(k-2)

#include<iostream>
#include<vector>
using namespace std;
#define MAX_N 1000
class BigInt : public vector<int>{
public :
    BigInt() { push_back(0); }
    BigInt(int x) {
        push_back(x);
        proccess_digit();
        return ;
    }
    
    BigInt &operator+=(const BigInt &a) {
        for (int i = 0; i < a.size(); i++) {
            if (i < size()) at(i) += a[i];
            else push_back(a[i]);
        }
        proccess_digit();
        return *this;
    }

    BigInt &operator*=(const int x) {
        for (int i = 0; i < size(); i++) at(i) *= x;
        proccess_digit();
        return *this;
    }

    BigInt operator*(const int x) {
        BigInt ret(*this);
        ret *= x;
        return ret;
    }

private :
    void proccess_digit() {
        for (int i = 0; i < size(); i++) {
            if (at(i) < 100000) continue;
            if (i + 1 == size()) push_back(0);
            at(i + 1) += at(i) / 100000;
            at(i) %= 100000;
        }
        return ;
    }
};

BigInt dp[MAX_N]={0};
ostream &operator<<(ostream &out, const BigInt &a) {
    out << a[a.size() - 1];
    for (int i = int(a.size()) - 2; i >= 0; i--) {
        int num = a[i];
        for (int j = 10000; j > 0; j /= 10) {
            out << a[i] % (j * 10) / j;
        }
    }
    return out;
}

int main()
{
	int n,k;
	cin>>n>>k;
	dp[1]=k;
	dp[2]=k*(k-1);
	dp[3]=k*(k-1)*(k-2);
	for(int i=4;i<=n;i++)
	{
		dp[i]=dp[i-1]*(k-2);
		dp[i]+=dp[i-2]*(k-1);
	}
	cout<<dp[n];
	return 0;
}


2.扔鸡蛋

定义鸡蛋的硬度为 k,则代表鸡蛋最高从 k 楼扔下来不会碎掉,现在给你 n 个硬度相同的鸡蛋,楼高为 m,问最坏情况下最少测多少次,可以测出鸡蛋的硬度。

2FB2A117A30743DDB39CA46A537BADD2.jpg

输入
输入两个数字 n,m(1≤n≤32,1≤m<231),代表 n 个鸡蛋和 m 层楼。

输出
输出一行整数,代表最坏情况下最少测多少次可以测出鸡蛋的硬度。

样例输入1

2 100

样例输出1

14

样例输入2

1 5

样例输出2

5


动归状态: dp[ i ][ j ]:i个鸡蛋测j层楼最多最少需要的次数。
动归方程:
在这里插入图片描述
代码实现:

#include<iostream>
using namespace std;
#define MAX_N 32
#define MAX_M 100000
int dp[MAX_N+5][MAX_M+5]={0};
int main()
{
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=m;i++)
	dp[1][i]=i;
	for(int i=2;i<=n;i++)
	{
		dp[i][1]=1;
		for(int j=2;j<=m;j++)
		{
			dp[i][j]=m;
			for(int k=1;k<=j;k++)
			{
				dp[i][j]=min(dp[i][j],max(dp[i][j-k],dp[i-1][k-1])+1);
			}
		}
	}
	cout<<dp[n][m];
	return 0;
 } 

优化版本
动归状态: dp[ i ][ k ]:i个鸡蛋扔k次最少最多能测的楼层数。
动归方程:
在这里插入图片描述

代码实现:

#include<iostream>
using namespace std;
#define MAX_N 32
#define MAX_K 1000
long long dp[MAX_N+5][MAX_K+5]={0};
int main()
{
	long long n,m;
	cin>>n>>m;
	if(n==1)
	{
		cout<<m;
		return 0;
	}
	for(int i=1;i<=1000;i++)
	dp[1][i]=i;
	for(int i=2;i<=n;i++)
	{
		for(int k=1;k<=1000;k++)
		{
			dp[i][k]=dp[i-1][k-1]+dp[i][k-1]+1;
		}
	}
	for(int k=1;k<=1000;k++)
	{
		if(dp[n][k]<m)continue;
		cout<<k;
		break;
	}
	return 0;
 } 


3.转移过程

1.切割回文

题目描述
给出一个字符串S,问对字符串S最少切几刀,使得分成的每一部分都是一个回文串(注意:单一字符是回文串)

87699633.jpg

输入
一个长度为n(1≤n≤500000)​的字符串S,只包含小写字母。

输出
输出一个整数,代表所切的最少刀数。

样例输入

sehuhzzexe

样例输出

4


原做法为区间dp,时间复杂度为O(n3)。

优化

动归状态: dp[ i ]:从1到i位置最少切多少刀。
动归方程:

在这里插入图片描述
代码实现:

#include<iostream>
#include<cstring>
#include<vector>
using namespace std;
#define MAX_N 500000
char arr[MAX_N+5]={0};
int dp[MAX_N+5]={0};
vector<int>c[MAX_N+5];
//记录可以和i位置构成回文串的所有j位置,避免后面的重复计算。
void extract(int i,int j)
{
	while(arr[i]==arr[j])
	{
		c[i].push_back(j);
		i++,j--;
	}
	return ;
}
int main()
{
	cin>>arr+1;
	int len=strlen(arr+1);
	dp[1]=0;
	for(int i=1;i<=len;i++)
	{
		extract(i,i);
		extract(i,i+1);
	 } 
	for(int i=2;i<=len;i++)
	{
		dp[i]=i;
		for(auto j:c[i])
		{
			if(j==1)dp[i]=0;
			else dp[i]=min(dp[i],dp[j-1]+1);
		}
	} 
	cout<<dp[len];
	return 0;
}


2.最长上升子序列

题目描述
有一个数字序列,求其中最长严格上升子序列的长度

在这里插入图片描述

输入
输入一个数字n (1≤n≤1000000),代表数字序列的长度。

后跟 n​ 个整数,第 i​ 个整数 ai(1≤ai≤10000)​,代表数字序列中的第 i​ 个值。

输出
输出一个整数,代表所求的最长严格上升子序列的长度。

样例输入

10
3 2 5 7 4 5 7 9 6 8

样例输出

5


代码实现

#include<iostream>
using namespace std;
#define MAX_N 1000000
#define MAX_K 10000
int dp[MAX_N+5]={0};
int len[MAX_K+5]={0};
int binary_search(int x,int ans)
{
	int head=0,tail=ans,mid;
	while(head<tail)
	{
		mid=(head+tail+1)/2;
		if(len[mid]<x)head=mid;
		else tail=mid-1;
	}
	return head;
}
int main()
{
	int n;
	cin>>n;
	len[0]=-1;
	int ans=0;
	for(int i=1,x;i<=n;i++)
	{
		cin>>x;
		dp[i]=binary_search(x,ans)+1;
		len[dp[i]]=x;
		if(dp[i]>ans)ans=dp[i];
	}
	cout<<ans;
	return 0;
}


3.多重背包

题目描述
给有一个能承重 V 的背包,和n种物品,每种物品的数量有限多,我们用重量、价值和数量的三元组来表示一个物品,第 i 件物品表示为(Vi,Wi,Si),问在背包不超重的情况下,得到物品的最大价值是多少?

在这里插入图片描述

输入
第一行输入两个数V、n,分别代表背包的最大承重和物品种类数。

接下来 n 行,每行三个数 Vi、Wi、Si,分别代表第 i 种物品的重量、价值和数量。

输出
输出一个整数,代表在背包不超重情况下所装物品的最大价值。

样例输入1

15 4
4 10 5
3 7 4
12 12 2
9 8 7

样例输出1

37


优化1-拆分优化
在原先的做法种中,我们依次枚举了以一种物品中的每一个,但是这种效率比较低,我们可以参考二进制,例如一个数14,我们可以将其拆分为1,2,4,8,7四部分,他们可以组合为任意数,这样一来,我们之前要进行的的14次枚举现在只需要4次。
代码实现

#include<iostream>
using namespace std;
#define MAX_N 100000
int dp[MAX_N+5];
int main()
{
	int V,n;
	cin>>V>>n;
	for(int i=1,v,w,s;i<=n;i++)
	{
		cin>>v>>w>>s;
		for(int k=1;s;s-=k,k*=2)
		{
			k=min(k,s);
			for(int j=V;j>=k*v;j--)
			{
				dp[j]=max(dp[j],dp[j-k*v]+k*w);
			}
		}
	}
	cout<<dp[V];
	return 0;
 } 

优化2-单调队列

#include<iostream>
#include<deque>
using namespace std;
#define MAX_V 100000
#define MAX_N 100
int dp[MAX_N+5][MAX_V+5];
int main()
{
	int V,n;
	cin>>V>>n;
	for(int i=1,v,w,s;i<=n;i++)
	{
		cin>>v>>w>>s;
		for(int j=0;j<v;j++)
		{
			deque<int>q;
			for(int k=j;k<=V;k+=v)
			{
				dp[i-1][k]-=k/v*w;
				while(!q.empty()&&dp[i-1][q.back()]<dp[i-1][k])q.pop_back();
				q.push_back(k);
				if((k-q.front())/v>s)q.pop_front();
				dp[i][k]=dp[i-1][q.front()]+k/v*w;
			}
		}
	}
	cout<<dp[n][V];
	return 0;
}


4.矩形

在一个黑白相间的矩形中,问有多少个全白色的子矩形。

18475740.jpg

输入
第一行输入两个数字 n,m(2≤n,m≤1000),代表矩形的长和宽。

接下来 n 行,每行 m 个数字,0 代表黑色格子,1 代表白色格子。

输出
输出一个整数,代表全白色子矩形的数量,结果可能过大,输出时请对 100007 取余。

样例输入1

6 6
0 1 1 1 1 1
1 1 0 1 1 1
1 1 1 1 1 1
1 1 1 0 1 1
1 1 1 1 0 1
1 0 1 1 1 1

样例输出1

152


动归状态: dp[ i ][ j ]:以i,j位置开始的矩形的数量。
f[ i ][ j ]:i,j位置即下面的连续白色块数量。
动归方程:
dp[ i ][ j ]=f[ i ][ j ]*(k-j)+dp[ i ][ k ] (k为本行第一个小于f[ i ][ j ]的位置)
代码实现:
优化前

#include<iostream>
using namespace std;
#define MAXSIZE 1000
long long dp[MAXSIZE+5][MAXSIZE+5]={0};
int f[MAXSIZE+5][MAXSIZE+5]={0};
int arr[MAXSIZE+5][MAXSIZE+5];
int main()
{
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			cin>>arr[i][j];
		}
	}
	for(int i=n;i>=1;i--)
	{
		for(int j=1;j<=m;j++)
		{
			if(arr[i][j]==0)
			{
				f[i][j]=0; 
				continue;
			}
			f[i][j]=f[i+1][j]+1;
		}
	}	
	long long ans=0;
	for(int i=1;i<=n;i++)
	{
		for(int j=m;j>=1;j--)
		{
			int len=0;
			for(int k=j;k<=m;k++)
			{
				if(f[i][j]>f[i][k])break;
				len++;
			}
			dp[i][j]=f[i][j]*len+dp[i][j+len];
			dp[i][j]%=100007;
			ans+=dp[i][j];
			ans%=100007;
		}
	}

	cout<<ans;
	return 0;
}

优化-单调栈(维护最近小于关系)

#include<iostream>
#include<stack>
using namespace std;
#define MAXSIZE 1000
long long dp[MAXSIZE+5]={0};
int f[MAXSIZE+5][MAXSIZE+5]={0};
int arr[MAXSIZE+5][MAXSIZE+5];
int main()
{
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			cin>>arr[i][j];
		}
	}
	for(int i=n;i>=1;i--)
	{
		for(int j=1;j<=m;j++)
		{
			if(arr[i][j]==0)
			{
				f[i][j]=0; 
				continue;
			}
			f[i][j]=f[i+1][j]+1;
		}
	}	
	long long ans=0;
	for(int i=1;i<=n;i++)
	{
		stack<int>s;
		f[i][m+1]=-1;
		s.push(m+1);
		for(int j=m;j>=1;j--)
		{
			while(f[i][s.top()]>=f[i][j])s.pop();
			dp[j]=f[i][j]*(s.top()-j)+dp[s.top()];
			dp[j]%=100007;
			ans+=dp[j];
			ans%=100007;
			s.push(j);
		}
	}
	cout<<ans; 
	return 0;
}


4.斜率优化

1.古老的打字机

题目描述
有一台古老的打字机和一篇待打印的文章,文章中有 n 个字符,每个字符会有一个消耗值 Ci, 打字机工作一次会打印若干连续的 k 个字符,同时打字机会有磨损,打字机的单次磨损计算公式为:

在这里插入图片描述

其中 M 是打字机启动一次的固定磨损值,现在给你 n 个字符的消耗值,问你打字机顺序打印出这 n 个字符的最小磨损值为多少?

输入
第一行输入两个数字,n,M(1≤n≤106,1≤M≤104) 代表文章中字符数量和打字机单次启动的固定磨损值。

第二行输入 n 个数字,第 i 个数字代表文章中第 i 个字符的磨损值 Ci(1≤Ci≤100)。

输出
输出一个整数,代表打字机顺序打完 n 个字符的最小磨损值

样例输入1

6 40
3 3 6 5 1 2

样例输出1

256

思路:如果我们使用常规的思路,很容易会想到将状态定义为dp[ i ]:前i个数字的最小磨损值。
在这里插入图片描述
但是此时的时间复杂度为O(n2),会超时。
优化策略:我们将dp[ i ]的公式拆开,得到:
在这里插入图片描述
查找值是可以记录的,确定值是一个常数,只有混合值是可变的,如果没有混合值,那么时间复杂度可以降低为O(n),如何可以消除混合值的影响?
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
由此可以看出,只要呈现出上图的情况,k一定不是候选答案,可直接删除。
在这里插入图片描述
因此,备选答案的集合一定是这个样子,现在我们要怎么在上面的集合中找到备选值呢?
在这里插入图片描述
据此可以分析出,找到第一个大于2sum[ i ]斜率的g(x,y),x就是最终应选的值。
代码实现:

#include<iostream>
using namespace std;
#define MAX_N 1000000
#define SQ(a) ((a)*(a))
long long dp[MAX_N+5];
int q[MAX_N+5];
long long f[MAX_N+5];
long long sum[MAX_N+5];
long long n,M;
double slope(int a,int b)
{
	return (1.0*(f[a]-f[b]))/(1.0*(sum[a]-sum[b]));
}
void set(int a,int b)
{
	dp[b]=dp[a]+SQ(sum[b]-sum[a])+M;
	f[b]=dp[b]+SQ(sum[b]);
	return ;
}
int main()
{
	cin>>n>>M;
	int head=0,tail=0;
	q[tail++]=0;
	sum[0]=0;
	for(int i=1;i<=n;i++)
	{
		cin>>sum[i];
		sum[i]+=sum[i-1];
	}
	for(int i=1;i<=n;i++)
	{
		while(tail-head>=2&&slope(q[head],q[head+1])<2*sum[i])head++;
		set(q[head],i);
		while(tail-head>=2&&slope(q[tail-1],q[tail-2])>slope(q[tail-1],i))tail--;
		q[tail++]=i;
	}
	cout<<dp[n];
	return 0;
}

练习题

1.低价购买

题目描述

“低价购买”这条建议是在奶牛股票市场取得成功的一半规则。要想被认为是伟大的投资者,你必须遵循以下的问题建议:“低价购买;再低价购买”。每次你购买一支股票,你必须用低于你上次购买它的价格购买它。买的次数越多越好!你的目标是在遵循以上建议的前提下,求你最多能购买股票的次数。你将被给出一段时间内一支股票每天的出售价,你可以选择在哪些天购买这支股票。每次购买都必须遵循“低价购买;再低价购买”的原则。写一个程序计算最大购买次数。

这里是某支股票的价格清单:

日期 1 2 3 4 5 6 7 8 9 10 11 12 价格 68 69 54 64 68 64 70 67 78 62 98 87 \def\arraystretch{1.5} \begin{array}{|c|c|c|c|c|c|c|c|c|c|c|c|c|}\hline \textsf{日期} & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 & 10 & 11 & 12 \cr\hline \textsf{价格} & 68 & 69 & 54 & 64 & 68 & 64 & 70 & 67 & 78 & 62& 98 & 87 \cr\hline \end{array} 日期价格168269354464568664770867978106211981287

最优秀的投资者可以购买最多 4 4 4 次股票,可行方案中的一种是:

日期 2 5 6 10 价格 69 68 64 62 \def\arraystretch{1.5} \begin{array}{|c|c|c|c|c|}\hline \textsf{日期} & 2 & 5 & 6 & 10 \cr\hline \textsf{价格} & 69 & 68 & 64 & 62 \cr\hline \end{array} 日期价格2695686641062

输入格式

第一行共一个整数 N   ( 1 ≤ N ≤ 5000 ) N\ (1 \le N \le 5000) N (1N5000),股票发行天数

第二行一行 N N N 个整数,是每天的股票价格。保证是大小不超过 2 16 2^{16} 216 的正整数。

输出格式

输出共一行两个整数,分别为最大购买次数和拥有最大购买次数的方案数(数据保证 $ \le 2^{31}$)当二种方案“看起来一样”时(就是说它们构成的价格队列一样的时候),这 2 2 2 种方案被认为是相同的。

样例 #1
样例输入 #1

12
68 69 54 64 68 64 70 67 78 62 98 87

样例输出 #1

4 2

思路: 本题求解包含了两部分,一部分是求最大购买次数,另一部分是求最大购买次数的方案数。第一部分的求解实际上是一个最长下降子序列的问题,状态定义dp[ i ]:以i结尾的最大购买方案数,dp[ i ]=max(dp[ j ])+1(0=<j<i&&val[ j ]>val[ i ])。对于第二部分的求解,我们可以定义f[ i ]为以i结尾的最大购买次数的方案数,f[ i ]=sum(f[ j ]),其中0=<j<i&&val[ j ]>val[ i ]&&dp[ i ]=dp[ j ]+1。对于如何去重,我们可以考虑,如果val[ a ]=val[ b ],且dp[ a ]=dp[ b ]&&b>a,那么a的方案数一定也包含在b的方案里面,我们可以直接令f[ a ]=0。
代码实现:

#include<iostream>
#include<cinttypes>
using namespace std;
#define MAX_N 5000
int dp[MAX_N+5]={0};
int val[MAX_N+5];
int f[MAX_N+5];
int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	cin>>val[i];
	val[0]=INT32_MAX;
	int max_len=0;
	f[0]=1;
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<i;j++)
		{
			if(val[j]<=val[i])continue;
			dp[i]=max(dp[i],dp[j]+1);
		}
		for(int j=0;j<i;j++)
		{
			if(val[j]<val[i])continue;
			if(val[i]<val[j]&&dp[i]==dp[j]+1)f[i]+=f[j];
			if(val[i]==val[j]&&dp[i]==dp[j])f[j]=0;
		}
		max_len=max(max_len,dp[i]);
	}
	int num=0;
	for(int i=1;i<=n;i++)
	if(dp[i]==max_len)num+=f[i];
	cout<<max_len<<" "<<num;
	return 0;
 } 


2.杂务

题目描述

John 的农场在给奶牛挤奶前有很多杂务要完成,每一项杂务都需要一定的时间来完成它。比如:他们要将奶牛集合起来,将他们赶进牛棚,为奶牛清洗乳房以及一些其它工作。尽早将所有杂务完成是必要的,因为这样才有更多时间挤出更多的牛奶。

当然,有些杂务必须在另一些杂务完成的情况下才能进行。比如:只有将奶牛赶进牛棚才能开始为它清洗乳房,还有在未给奶牛清洗乳房之前不能挤奶。我们把这些工作称为完成本项工作的准备工作。至少有一项杂务不要求有准备工作,这个可以最早着手完成的工作,标记为杂务 1 1 1

John 有需要完成的 n n n 个杂务的清单,并且这份清单是有一定顺序的,杂务 k   ( k > 1 ) k\ (k>1) k (k>1) 的准备工作只可能在杂务 1 1 1 k − 1 k-1 k1 中。

写一个程序依次读入每个杂务的工作说明。计算出所有杂务都被完成的最短时间。当然互相没有关系的杂务可以同时工作,并且,你可以假定 John 的农场有足够多的工人来同时完成任意多项任务。

输入格式

第1行:一个整数 n   ( 3 ≤ n ≤ 10 , 000 ) n\ (3 \le n \le 10{,}000) n (3n10,000),必须完成的杂务的数目;

2 2 2 n + 1 n+1 n+1 行,每行有一些用空格隔开的整数,分别表示:

  • 工作序号(保证在输入文件中是从 1 1 1 n n n 有序递增的);
  • 完成工作所需要的时间 l e n   ( 1 ≤ l e n ≤ 100 ) len\ (1 \le len \le 100) len (1len100)
  • 一些必须完成的准备工作,总数不超过 100 100 100 个,由一个数字 0 0 0 结束。有些杂务没有需要准备的工作只描述一个单独的 0 0 0

保证整个输入文件中不会出现多余的空格。

输出格式

一个整数,表示完成所有杂务所需的最短时间。

样例 #1

样例输入 #1

7
1 5 0
2 2 1 0
3 3 2 0
4 6 1 0
5 1 2 4 0
6 8 2 4 0
7 4 3 5 6 0

样例输出 #1

23

思路:状态定义为dp[ i ]:完成任务i时的最短时长。dp[ i ]=max(dp[ j ]+t[ i ]) (j为i的前置任务)

#include<iostream>
using namespace std;
#define MAX_N 10000
int dp[MAX_N+5];
int main()
{
	int n;
	cin>>n;
	int ans=0;
	for(int i=1,t,j;i<=n;i++)
	{
		cin>>t>>t;
		dp[i]=t;
		while(cin>>j)
		{
			if(j==0)break;
			dp[i]=max(dp[i],dp[j]+t);
		}
		ans=max(ans,dp[i]);
	}
	cout<<ans;
}


3.最大子段和

题目描述

给出一个长度为 n n n 的序列 a a a,选出其中连续且非空的一段使得这段和最大。

输入格式

第一行是一个整数,表示序列的长度 n n n

第二行有 n n n 个整数,第 i i i 个整数表示序列的第 i i i 个数字 a i a_i ai

输出格式

输出一行一个整数表示答案。

样例 #1

样例输入 #1

7
2 -4 3 -1 2 -4 3

样例输出 #1

4

提示

样例 1 解释

选取 [ 3 , 5 ] [3, 5] [3,5] 子段 { 3 , − 1 , 2 } \{3, -1, 2\} {3,1,2},其和为 4 4 4

数据规模与约定

  • 对于 40 % 40\% 40% 的数据,保证 n ≤ 2 × 1 0 3 n \leq 2 \times 10^3 n2×103
  • 对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 2 × 1 0 5 1 \leq n \leq 2 \times 10^5 1n2×105 − 1 0 4 ≤ a i ≤ 1 0 4 -10^4 \leq a_i \leq 10^4 104ai104

思路: 实现1:状态定义dp[ i ]:以第i个位置结尾的最大子段和,dp[ i ]=val[ i ]+max(0,dp[ i-1 ])。实现2:维护前缀和数组,使用单调队列。
代码实现1

#include<iostream>
#include<deque>
using namespace std;
#define MAX_N 200000
long long dp[MAX_N+5];
int main()
{
	int n;
	cin>>n;
	dp[0]=0;
	long long ans=-100000;
	for(int i=1;i<=n;i++)
	{
		cin>>dp[i];
		if(dp[i-1]>0)dp[i]+=dp[i-1];
		ans=max(ans,dp[i]);
	}
	cout<<ans;
	return 0;
}

代码实现2

#include<iostream>
#include<deque>
using namespace std;
#define MAX_N 200000
long long sum[MAX_N+5];
int main()
{
	int n;
	cin>>n;
	sum[0]=0;
	for(int i=1;i<=n;i++)
	{
		cin>>sum[i];
		sum[i]+=sum[i-1];
	} 
	deque<int>q;
	q.push_back(0);
	long long ans=-100000;
	for(int i=1;i<=n;i++)
	{
		ans=max(ans,sum[i]-sum[q.front()]);
		while(!q.empty()&&sum[i]<sum[q.back()])q.pop_back();
		q.push_back(i);
	}
	cout<<ans;
	return 0;
}


4.NASA的食物计划

题目背景

NASA(美国航空航天局)因为航天飞机的隔热瓦等其他安全技术问题一直大伤脑筋,因此在各方压力下终止了航天飞机的历史,但是此类事情会不会在以后发生,谁也无法保证。所以,在遇到这类航天问题时,也许只能让航天员出仓维修。但是过多的维修会消耗航天员大量的能量,因此 NASA 便想设计一种食品方案,使体积和承重有限的条件下多装载一些高卡路里的食物。

题目描述

航天飞机的体积有限,当然如果载过重的物品,燃料会浪费很多钱,每件食品都有各自的体积、质量以及所含卡路里。在告诉你体积和质量的最大值的情况下,请输出能达到的食品方案所含卡路里的最大值,当然每个食品只能使用一次。

输入格式

第一行 2 2 2 个整数,分别代表体积最大值 h h h 和质量最大值 t t t

第二行 1 1 1 个整数代表食品总数 n n n

接下来 n n n 行每行 3 3 3 个数 体积 h i h_i hi,质量 t i t_i ti,所含卡路里 k i k_i ki

输出格式

一个数,表示所能达到的最大卡路里(int 范围内)

样例 #1

样例输入 #1

320 350
4
160 40 120
80 110 240
220 70 310
40 400 220

样例输出 #1

550

提示

对于 100 % 100\% 100% 的数据, h , t , h i , t i ≤ 400 h,t,h_i,t_i \le 400 h,t,hi,ti400 n ≤ 50 n \le 50 n50 k i ≤ 500 k_i \le 500 ki500


思路:本题为有二重限制的01背包问题,状态定义dp[ i ][ j ][ k ]:i个物品体积和质量限制分别为j,k限制下的最大卡路里。
dp[ i ][ j ][ k ]=max(dp[ i ][ j ][ k ],dp[ i ][ j-hi ][ k-ti ]+ki)。

代码实现:

#include<iostream>
using namespace std;
#define MAX_N 50
#define MAX_H 400
int dp[MAX_H+5][MAX_H+5]={0};
int main()
{
	int h,t,n;
	cin>>h>>t>>n;
	for(int i=1,hi,ti,ki;i<=n;i++)
	{
		cin>>hi>>ti>>ki;
		for(int j=h;j>=hi;j--)
		{
			for(int k=t;k>=ti;k--)
			{
				dp[j][k]=max(dp[j][k],dp[j-hi][k-ti]+ki);
			}
		}
	}
	cout<<dp[h][t];
	return 0;
}


5.魔族密码

题目背景

风之子刚走进他的考场,就……

花花:当当当当~~偶是魅力女皇——花花!!^^(华丽出场,礼炮,鲜花)

风之子:我呕……(杀死人的眼神)快说题目!否则……-_-###

题目描述

花花:……咦好冷我们现在要解决的是魔族的密码问题(自我陶醉:搞不好魔族里面还会有人用密码给我和菜虫写情书咧,哦活活,当然是给我的比较多拉*_*)。

魔族现在使用一种新型的密码系统。每一个密码都是一个给定的仅包含小写字母的英文单词表,每个单词至少包含 1 1 1 个字母,至多 75 75 75 个字母。如果在一个由一个词或多个词组成的表中,除了最后一个以外,每个单词都被其后的一个单词所包含,即前一个单词是后一个单词的前缀,则称词表为一个词链。例如下面单词组成了一个词链:

  • i \verb!i! i
  • int \verb!int! int
  • integer \verb!integer! integer

但下面的单词不组成词链:

  • integer \verb!integer! integer
  • intern \verb!intern! intern

现在你要做的就是在一个给定的单词表中取出一些词,组成最长的词链,就是包含单词数最多的词链。将它的单词数统计出来,就得到密码了。

风之子:密码就是最长词链所包括的单词数阿……

输入格式

这些文件的格式是,第一行为单词表中的单词数 N N N 1 ≤ N ≤ 2000 1 \le N \le 2000 1N2000),下面每一行有一个单词,按字典顺序排列,中间也没有重复的单词。

输出格式

输出共一行,一个整数,表示密码。

样例 #1

样例输入 #1

5
i
int
integer
intern
internet

样例输出 #1

4

思路:本题为最长上升子序列问题的变形,只是将值得上升关系变成了字符串得前缀关系。状态定义:dp[ i ]:以i位置结尾得最长词链单词数。dp[ i ]=max(dp[ j ])+1(arr[ j ]包含于arr[ i ]&&j<i)。
代码实现

#include<iostream>
using namespace std;
#define MAX_N 2000
int dp[MAX_N+5];
string arr[MAX_N+5];
bool involve(int i,int j)
{
	if(arr[i].size()>=arr[j].size())return 0;
	int n=arr[i].size();
	for(int l=0;l<n;l++)
	if(arr[i][l]!=arr[j][l])return 0;
	return 1;
}
int main()
{
	int n;
	cin>>n;
	int ans=1;
	for(int i=1;i<=n;i++)dp[i]=1;
	for(int i=1;i<=n;i++)cin>>arr[i];
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<i;j++)
		{
			if(involve(j,i))dp[i]=max(dp[i],dp[j]+1);
		}
		ans=max(ans,dp[i]);
	}
	cout<<ans;
	return 0;
}


6.找啊找啊找GF

题目背景

“找啊找啊找 GF,找到一个好 GF,吃顿饭啊拉拉手,你是我的好 GF。再见。”

“诶,别再见啊…”

七夕… 七夕… 七夕这个日子,对于 sqybi 这种单身的菜鸟来说是多么的痛苦… 虽然他听着这首叫做“找啊找啊找 GF”的歌,他还是很痛苦。为了避免这种痛苦,sqybi 决定要给自己找点事情干。他去找到了七夕模拟赛的负责人 zmc MM,让她给自己一个出题的任务。经过几天的死缠烂打,zmc MM 终于同意了。

但是,拿到这个任务的 sqybi 发现,原来出题比单身更让人感到无聊 -_- … 所以,他决定了,要在出题的同时去办另一件能够使自己不无聊的事情——给自己找 GF。

题目描述

sqybi 现在看中了 n n n 个 MM,我们不妨把她们编号 1 1 1 n n n。请 MM 吃饭是要花钱的,我们假设请 i i i 号 MM 吃饭要花 r m b [ i ] rmb[i] rmb[i] 块大洋。而希望骗 MM 当自己 GF 是要费人品的,我们假设请第 i i i 号 MM 吃饭试图让她当自己 GF 的行为(不妨称作泡该 MM)要耗费 r p [ i ] rp[i] rp[i] 的人品。而对于每一个 MM 来说,sqybi 都有一个对应的搞定她的时间,对于第 i i i 个 MM 来说叫做 t i m e [ i ] time[i] time[i]。sqybi 保证自己有足够的魅力用 t i m e [ i ] time[i] time[i] 的时间搞定第 i i i 个 MM _

sqybi 希望搞到尽量多的 MM 当自己的 GF,这点是毋庸置疑的。但他不希望为此花费太多的时间(毕竟七夕赛的题目还没出),所以他希望在保证搞到 MM 数量最多的情况下花费的总时间最少。

sqybi 现在有 m m m 块大洋,他也通过一段时间的努力攒到了 r r r 的人品(这次为模拟赛出题也攒 rp 哦~~)。他凭借这些大洋和人品可以泡到一些 MM。他想知道,自己泡到最多的 MM 花费的最少时间是多少。

注意 sqybi 在一个时刻只能去泡一个 MM ——如果同时泡两个或以上的 MM 的话,她们会打起来的…

输入格式

输入的第一行是 n n n,表示 sqybi 看中的 MM 数量。

接下来有 n n n 行,依次表示编号为 1 , 2 , 3 , … , n 1, 2, 3, \ldots , n 1,2,3,,n 的一个 MM 的信息。每行表示一个 MM 的信息,有三个整数: r m b rmb rmb r p rp rp t i m e time time

最后一行有两个整数,分别为 m m m r r r

输出格式

你只需要输出一行,其中有一个整数,表示 sqybi 在保证 MM 数量的情况下花费的最少总时间是多少。

样例 #1

样例输入 #1

4
1 2 5
2 1 6
2 2 2
2 2 3
5 5

样例输出 #1

13

提示

sqybi 说:如果题目里说的都是真的就好了…

sqybi 还说,如果他没有能力泡到任何一个 MM,那么他就不消耗时间了(也就是消耗的时间为 0 0 0),他要用这些时间出七夕比赛的题来攒 rp…

【数据规模】

对于 20 % 20 \% 20% 的数据, 1 ≤ n ≤ 10 1 \le n \le 10 1n10
对于 100 % 100 \% 100% 的数据, 1 ≤ r m b ≤ 100 1 \le rmb \le 100 1rmb100 1 ≤ r p ≤ 100 1 \le rp \le 100 1rp100 1 ≤ t i m e ≤ 1000 1 \le time \le 1000 1time1000
对于 100 % 100 \% 100% 的数据, 1 ≤ m , r , n ≤ 100 1 \le m, r, n \le 100 1m,r,n100


思路:本题实际上也是一个多条件得01背包问题,不同的是本题除了要维护一个最优值,还要维护一个与最优值相关的值。

代码实现

#include<iostream>
using namespace std;
int mm[105][3];
int dp[105][105]={0};
int tt[105][105]={0};
int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	cin>>mm[i][0]>>mm[i][1]>>mm[i][2];
	int m,r;
	cin>>m>>r;
	for(int i=1;i<=n;i++)
	{
		for(int j=m;j>=mm[i][0];j--)
		{
			for(int k=r;k>=mm[i][1];k--)
			{
				if(dp[j-mm[i][0]][k-mm[i][1]]+1<dp[j][k])continue;
				else if(dp[j-mm[i][0]][k-mm[i][1]]+1==dp[j][k])
				{
					tt[j][k]=min(tt[j][k],tt[j-mm[i][0]][k-mm[i][1]]+mm[i][2]);
				}
				else{
					dp[j][k]=dp[j-mm[i][0]][k-mm[i][1]]+1;
					tt[j][k]=tt[j-mm[i][0]][k-mm[i][1]]+mm[i][2];
				}
			
			}
		}
	}
	if(tt[m][r]==100000)cout<<0;
	else cout<<tt[m][r];
	
	return 0;
}


7.书本整理

题目描述

Frank 是一个非常喜爱整洁的人。他有一大堆书和一个书架,想要把书放在书架上。书架可以放下所有的书,所以 Frank 首先将书按高度顺序排列在书架上。但是 Frank 发现,由于很多书的宽度不同,所以书看起来还是非常不整齐。于是他决定从中拿掉k本书,使得书架可以看起来整齐一点。

书架的不整齐度是这样定义的:每两本书宽度的差的绝对值的和。例如有 4 4 4 本书:

1 × 2 1 \times 2 1×2
5 × 3 5 \times 3 5×3
2 × 4 2 \times 4 2×4
3 × 1 3 \times 1 3×1

那么 Frank 将其排列整齐后是:

1 × 2 1 \times 2 1×2
2 × 4 2 \times 4 2×4
3 × 1 3 \times 1 3×1
5 × 3 5 \times 3 5×3

不整齐度就是 2 + 3 + 2 = 7 2+3+2=7 2+3+2=7

已知每本书的高度都不一样,请你求出去掉 k k k 本书后的最小的不整齐度。

输入格式

第一行两个数字 n n n k k k,代表书有几本,从中去掉几本( 1 ≤ n ≤ 100 , 1 ≤ k < n 1 \le n \le 100, 1 \le k<n 1n100,1k<n)。

下面的 n n n 行,每行两个数字表示一本书的高度和宽度,均小于等于 200 200 200

保证高度不重复

输出格式

一行一个整数,表示书架的最小不整齐度。

样例 #1

样例输入 #1

4 1
1 2
2 4
3 1
5 3

样例输出 #1

3

思路:将问题进行转换为n本书中选n-k本得最小不整齐度。状态定义dp[ i ][ j ]:前i本书选j本得最小不整齐度,
dp[ i ][ j ]=min(dp[ k ][ j ]+abs(val[ i ]-val[ k ]))。

代码实现

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
#define MAXSIZE 100
int dp[MAXSIZE+5][MAXSIZE+5];
struct Data{
	int l,r;
};
Data book[MAXSIZE+5];
bool cmp(const Data&a,const Data&b)
{
	return a.l<b.l;
}
int main()
{
	int n,K;
	cin>>n>>K;
	int k=n-K;
	for(int i=1;i<=n;i++)cin>>book[i].l>>book[i].r;
	sort(book+1,book+n+1,cmp);
	memset(dp,0x7f7f7f,sizeof(dp));
	for(int i=1;i<=n;i++)
	{
		dp[i][0]=0,dp[i][1]=0;
		for(int j=2;j<=min(i,k);j++)
		{
			for(int l=1;l<i;l++)
			{
				dp[i][j]=min(dp[i][j],dp[l][j-1]+abs(book[l].r-book[i].r));
			}
		}
	}
	int ans=0x7fffffff;
	for(int i=1;i<=n;i++)
	ans=min(ans,dp[i][k]);
	cout<<ans;
	return 0;
}


8.删数

题目描述

N N N 个不同的正整数 x 1 x_1 x1, x 2 x_2 x2, …, x N x_N xN 排成一排,我们可以从左边或右边去掉连续的 i i i ( 1 ≤ i ≤ n ) (1 \le i \le n) (1in) 个数(只能从两边删除数),剩下 N − i N-i Ni 个数,再把剩下的数按以上操作处理,直到所有的数都被删除为止。

每次操作都有一个操作价值,比如现在要删除从 i i i 位置到 k k k 位置上的所有的数。操作价值为 ∣ x i − x k ∣ × ( k − i + 1 ) |x_i-x_k| \times (k-i+1) xixk×(ki+1) ,如果只去掉一个数,操作价值为这个数的值。
问如何操作可以得到最大值,求操作的最大价值。

输入格式

第一行为一个正整数 N N N ;

第二行有 N N N 个用空格隔开的 N N N 个不同的正整数。

输出格式

一行,包含一个正整数,为操作的最大值

样例 #1

样例输入 #1

6
54 29 196 21 133 118

样例输出 #1

768

提示

【样例解释和说明】

说明,经过 3 3 3 次操作可以得到最大值,第一次去掉前面 3 3 3 个数: 54 54 54 29 29 29 196 196 196 ,操作价值为 426 426 426。第二次操作是在剩下的三个数 ( 21 , 133 , 118 ) (21,133,118) (21,133,118) 中去掉最后一个数 118 118 118,操作价值为 118 118 118。第三次操作去掉剩下的 2 2 2 个数: 21 21 21 133 133 133 ,操作价值为 224 224 224。操作总价值为 426 + 118 + 224 = 768 426+118+224=768 426+118+224=768

【数据范围】

3 ≤ N ≤ 100 3≤N≤100 3N100 1 ≤ x i ≤ 1000 1 \le x_i \le 1000 1xi1000


思路:将问题进行转化,本问题实际上就是求将原序列分成若干段,求总的操作价值。
状态定义dp[ i ]:前i个位置的最大操作价值。
dp[ i ]=max(dp[ j-1 ]+(i-j+1)*abs(val[ i ]-val[ j ]))与dp[ i-1 ]+val[ i ]中的最大值。

代码实现

#include<iostream>
using namespace std;
#define MAX_N 100
int dp[MAX_N+5]={0};
int arr[MAX_N+5];
int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	cin>>arr[i];
	for(int i=1;i<=n;i++)
	{
		dp[i]=dp[i-1]+arr[i];
		for(int j=1;j<i;j++)
		{
			dp[i]=max(dp[i],dp[j-1]+abs(arr[i]-arr[j])*(i-j+1));
		}
	}
	cout<<dp[n];
	return 0;
}


9.垃圾陷阱

题目描述

卡门――农夫约翰极其珍视的一条 Holsteins 奶牛――已经落了到 “垃圾井” 中。“垃圾井” 是农夫们扔垃圾的地方,它的深度为 D D D 2 ≤ D ≤ 100 2 \le D \le 100 2D100)英尺。

卡门想把垃圾堆起来,等到堆得与井同样高时,她就能逃出井外了。另外,卡门可以通过吃一些垃圾来维持自己的生命。

每个垃圾都可以用来吃或堆放,并且堆放垃圾不用花费卡门的时间。

假设卡门预先知道了每个垃圾扔下的时间 t t t 1 ≤ t ≤ 1000 1 \le t \le 1000 1t1000),以及每个垃圾堆放的高度 h h h 1 ≤ h ≤ 25 1 \le h \le 25 1h25)和吃进该垃圾能增加维持生命的时间 f f f 1 ≤ f ≤ 30 1 \le f \le 30 1f30),要求出卡门最早能逃出井外的时间,假设卡门当前体内有足够持续 10 10 10 小时的能量,如果卡门 10 10 10 小时内(不含 10 10 10 小时,维持生命的时间同)没有进食,卡门就将饿死。特别地,若体力值为 0 0 0 时吃下垃圾或逃出井外也不会饿死。

输入格式

第一行为两个整数, D D D G G G 1 ≤ G ≤ 100 1 \le G \le 100 1G100), G G G 为被投入井的垃圾的数量。

第二到第 G + 1 G+1 G+1 行每行包括三个整数: T T T 1 ≤ T ≤ 1000 1 \le T \le 1000 1T1000),表示垃圾被投进井中的时间; F F F 1 ≤ F ≤ 30 1 \le F \le 30 1F30),表示该垃圾能维持卡门生命的时间;和 H H H 1 ≤ H ≤ 25 1 \le H \le 25 1H25),该垃圾能垫高的高度。

输出格式

如果卡门可以爬出陷阱,输出一个整数,表示最早什么时候可以爬出;否则输出卡门最长可以存活多长时间。

样例 #1

样例输入 #1

20 4
5 4 9
9 3 2
12 6 10
13 1 1

样例输出 #1

13

提示

【样例说明】

卡门堆放她收到的第一个垃圾: h e i g h t = 9 \mathrm{height}=9 height=9

卡门吃掉她收到的第 2 2 2 个垃圾,使她的生命从 10 10 10 小时延伸到 13 13 13 小时;

卡门堆放第 3 3 3 个垃圾, h e i g h t = 19 \mathrm{height}=19 height=19

卡门堆放第 4 4 4 个垃圾, h e i g h t = 20 \mathrm{height}=20 height=20


思路:本题比较接近背包模型,状态定义dp[ i ][ j ]:前i个垃圾在高度为j时的最高存活时间。区别在于背包模型中不选的物品直接舍弃,而在本问题中,不选的垃圾用作垫高,动归转移方程为dp[ i ][ j ]=max(dp[ i-1 ][ j ]+f,dp[ i-1 ][ j-h ])。
代码实现:

#include<iostream>
#include<algorithm>
using namespace std;
#define MAXSIZE 100
int dp[MAXSIZE+5][2*MAXSIZE+5]={0};
struct Data{
	int t,f,h;
}arr[MAXSIZE+5];
bool cmp(const Data&a,const Data&b)
{
	return a.t<b.t;
}
int main()
{
	int D,G;
	cin>>D>>G;
	for(int i=1;i<=G;i++)
	cin>>arr[i].t>>arr[i].f>>arr[i].h;
	sort(arr+1,arr+G+1,cmp);
	dp[0][0]=10;
	for(int i=1;i<=G;i++)
	{
		for(int j=0;j<=D+30;j++)
		{
			if(dp[i-1][j]>=arr[i].t)
			{
				if(j+arr[i].h>=D)
				{
					cout<<arr[i].t;
					return 0;
				}
				dp[i][j]=dp[i-1][j]+arr[i].f;
			}
			if(j>=arr[i].h&&dp[i-1][j-arr[i].h]>=arr[i].t)
			{
				if(j>=D)
				{
					cout<<arr[i].t;
					return 0;
				}
				dp[i][j]=max(dp[i][j],dp[i-1][j-arr[i].h]);
			}
		}
	}
	int ans=0;
	for(int i=1;i<=G;i++)
	ans=max(ans,dp[i][0]);      
	cout<<ans;
	return 0;
 } 


10.最大正方形II

题目背景

忙完了学校的事,v 神终于可以做他的“正事”:陪女朋友散步。一天,他和女朋友走着走着,不知不觉就来到了一个千里无烟的地方。v 神正要往回走,如发现了一块牌子,牌子上有有一行小字和一张图,小字说道:“找到图上最大的交错正方形之后和我联系,这块地就是你的了。”在房价疯长的年代,v 神当然不愿错过这个机会,于是开始找了起来……以 v 神的能力当然找不出来了,你能帮 v 神找出来吗?

题目描述

图上有一个矩阵,由 N × M N\times M N×M 个格子组成,这些格子由两种颜色构成,黑色和白色。请找到面积最大的且内部是黑白交错(即两个相连的正方形颜色不能相同)的正方形。

输入格式

第一行两个整数 N N N M M M,分别表示行数和列数。接下来有 N N N 行,每行 M M M 个数, 0 0 0 1 1 1 分别表示这个格子是黑色或白色。

输出格式

仅有一行,表示满足条件最大正方形的边长。

样例 #1

样例输入 #1

3 3
0 1 0
1 0 0
1 1 1

样例输出 #1

2

提示

样例解释

( 1 , 1 ) (1,1) (1,1) ( 2 , 2 ) (2,2) (2,2) 这个正方形是满足条件的,它的边长是 2 2 2

数据范围及约定

  • 对于 30 % 30\% 30% 的数据, N ≤ 20 N \le 20 N20
  • 对于 60 % 60\% 60% 的数据, N ≤ 300 N \le 300 N300
  • 对于 100 % 100\% 100% 的数据, N ≤ 1500 N \le 1500 N1500

思路:
状态定义dp[ i ][ j ]:以(i,j)位置结尾的正方形的最大边长。
dp[ i ][ j ]=max(dp[ i-1 ][ j-1 ](val[ i ][ j ]==val[ i-1 ][ j-1 ]时,否则为0),dp[ i ][ j-1 ](val[ i ][ j ]==val[ i ][ j-1 ]时,否则为0),dp[ i-1 ][ j ](val[ i ][ j ]==val[ i-1 ][ j ]时,否则为0)) (画图易得该结论)

代码实现:

#include<iostream>
#include<cstring>
using namespace std;
#define MAXSIZE 1500
int dp[MAXSIZE+5][MAXSIZE+5];
int arr[MAXSIZE+5][MAXSIZE+5];
int main()
{
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++)dp[i][1]=1;
	for(int i=1;i<=m;i++)dp[1][i]=1;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			scanf("%d",&arr[i][j]);
		}
	}
	int ans=1;
	for(int i=2;i<=n;i++)
	{
		for(int j=2;j<=m;j++)
		{
			int l1=0,l2=0,l3=0;
			if(arr[i-1][j-1]==arr[i][j])l1=dp[i-1][j-1];
			if(arr[i][j-1]!=arr[i][j])l2=dp[i][j-1];
			if(arr[i-1][j]!=arr[i][j])l3=dp[i-1][j];
			dp[i][j]=min(l1,min(l2,l3))+1;
			ans=max(ans,dp[i][j]);
		}
	}
	cout<<ans;
	return 0;
}


11.导弹拦截

题目描述

经过 11 11 11 年的韬光养晦,某国研发出了一种新的导弹拦截系统,凡是与它的距离不超过其工作半径的导弹都能够被它成功拦截。当工作半径为 0 0 0 时,则能够拦截与它位置恰好相同的导弹。但该导弹拦截系统也存在这样的缺陷:每套系统每天只能设定一次工作半径。而当天的使用代价,就是所有系统工作半径的平方和。

某天,雷达捕捉到敌国的导弹来袭。由于该系统尚处于试验阶段,所以只有两套系统投入工作。如果现在的要求是拦截所有的导弹,请计算这一天的最小使用代价。

输入格式

第一行包含 4 4 4 个整数 x 1 , y 1 , x 2 , y 2 x_1,y_1,x_2,y_2 x1,y1,x2,y2,每两个整数之间用一个空格隔开,表示这两套导弹拦截系统的坐标分别为 ( x 1 , y 1 ) , ( x 2 , y 2 ) (x_1,y_1), (x_2,y_2) (x1,y1),(x2,y2)。第二行包含 1 1 1 个整数 N N N,表示有 N N N 颗导弹。接下来 N N N 行,每行两个整数 x , y x,y x,y,中间用一个空格隔开,表示一颗导弹的坐标 ( x , y ) (x,y) (x,y)。不同导弹的坐标可能相同。

输出格式

一个整数,即当天的最小使用代价。

样例 #1

样例输入 #1

0 0 10 0
2
-3 3
10 0

样例输出 #1

18

样例 #2

样例输入 #2

0 0 6 0
5
-4 -2
-2 3
4 0
6 -2
9 1

样例输出 #2

30

提示

两个点 ( x 1 , y 1 ) , ( x 2 , y 2 ) (x_1,y_1),(x_2,y_2) (x1,y1),(x2,y2) 之间距离的平方是 ( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 2 (x_1-x_2)^2+(y_1-y_2)^2 (x1x2)2+(y1y2)2

两套系统工作半径 r 1 , r 2 r_1,r_2 r1,r2 的平方和,是指 r 1 , r 2 r_1,r_2 r1,r2 分别取平方后再求和,即 r 1 2 + r 2 2 r_1^2+r_2^2 r12+r22

样例 1 说明

样例 1 1 1 中要拦截所有导弹,在满足最小使用代价的前提下,两套系统工作半径的平方分别为 18 18 18 0 0 0

样例 2 说明

样例 2 2 2 中的导弹拦截系统和导弹所在的位置如下图所示。要拦截所有导弹,在满足最小使用代价的前提下,两套系统工作半径的平方分别为 20 20 20 10 10 10

【数据范围】。

  • 对于 10 % 10\% 10% 的数据, N = 1 N=1 N=1
  • 对于 20 % 20\% 20% 的数据, 1 ≤ N ≤ 2 1\le N\le 2 1N2
  • 对于 40 % 40\% 40% 的数据, 1 ≤ N ≤ 100 1\le N\le 100 1N100
  • 对于 70 % 70\% 70% 的数据, 1 ≤ N ≤ 1000 1\le N\le 1000 1N1000
  • 对于 100 % 100\% 100% 的数据, 1 ≤ N ≤ 1 0 5 1\le N\le 10^5 1N105,且所有坐标分量的绝对值都不超过 1000 1000 1000

思路:本题可以先将每个点距离(x1,y1)进行排序,排序后肯定有前i个数在系统1范围内,其于的在系统2范围内,ans=min(ans,DIS(arr[i].l,arr[i].r,x1,y1)+dis[i+1]),(dis[ i ]的含义为i位置后的点距离系统2的最大距离)。

#include<iostream>
#include<algorithm>
using namespace std;
#define MAXSIZE 100000
#define P2(a) ((a)*(a))
int x1,y1,x2,y2;
struct Data{
	int l,r;
}arr[MAXSIZE+5];
int dis[MAXSIZE+5]={0};
int DIS(int x1,int y1,int x,int y)
{
	return P2(x1-x)+P2(y1-y);
}
bool cmp(const Data&a,const Data&b)
{
	return DIS(a.l,a.r,x1,y1)<DIS(b.l,b.r,x1,y1);
}
int main()
{
	cin>>x1>>y1>>x2>>y2;
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	scanf("%d%d",&arr[i].l,&arr[i].r);
	sort(arr+1,arr+n+1,cmp);
	for(int i=n;i>=1;i--)
	{
		dis[i]=DIS(arr[i].l,arr[i].r,x2,y2);
		dis[i]=max(dis[i+1],dis[i]);
	}
	int ans=0x7fffffff;
	for(int i=1;i<=n;i++)
	ans=min(ans,DIS(arr[i].l,arr[i].r,x1,y1)+dis[i+1]);
	cout<<ans;
	return 0;
}


12.三元上升子序列

题目描述

Erwin 最近对一种叫 thair 的东西巨感兴趣。。。

在含有 n n n 个整数的序列 a 1 , a 2 , … , a n a_1,a_2,\ldots,a_n a1,a2,,an 中,三个数被称作thair当且仅当 i < j < k i<j<k i<j<k a i < a j < a k a_i<a_j<a_k ai<aj<ak

求一个序列中 thair 的个数。

输入格式

开始一行一个正整数 n n n,

以后一行 n n n 个整数 a 1 , a 2 , … , a n a_1,a_2,\ldots,a_n a1,a2,,an

输出格式

一行一个整数表示 thair 的个数。

样例 #1

样例输入 #1

4
2 1 3 4

样例输出 #1

2

样例 #2

样例输入 #2

5
1 2 2 3 4

样例输出 #2

7

提示

样例2 解释

7 7 7thair 分别是:

  • 1 2 3
  • 1 2 4
  • 1 2 3
  • 1 2 4
  • 1 3 4
  • 2 3 4
  • 2 3 4

数据规模与约定

  • 对于 30 % 30\% 30% 的数据 保证 n ≤ 100 n\le100 n100
  • 对于 60 % 60\% 60% 的数据 保证 n ≤ 2000 n\le2000 n2000
  • 对于 100 % 100\% 100% 的数据 保证 1 ≤ n ≤ 3 × 1 0 4 1 \leq n\le3\times10^4 1n3×104 1 ≤ a i ≤ 1 0 5 1\le a_i\leq 10^5 1ai105

思路:三元上升子序列的数量取决于二元上升子序列,二元上升子序列的数量取决于一元上升子序列,
dp[ i ][ 3 ]=sum(dp[ j ][ 2 ]) (j<i,val[ j ]<val[ i ]时)

代码实现:

#include<iostream>
using namespace std;
#define MAXSIZE 30000
int arr[MAXSIZE+5];
int dp[MAXSIZE+5][5]={0};
int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	cin>>arr[i];
	for(int i=1;i<=n;i++)
	dp[i][1]=1;
	for(int i=1;i<=n;i++)
	for(int j=1;j<i;j++)
	if(arr[j]<arr[i])dp[i][2]+=dp[j][1];
	long long ans=0;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<i;j++)
		if(arr[j]<arr[i])dp[i][3]+=dp[j][2];
		ans+=dp[i][3];
	} 
	cout<<ans;
	return 0;
 } 


13.多人背包

题目描述

求01背包前k优解的价值和

DD 和好朋友们要去爬山啦!

他们一共有 K 个人,每个人都会背一个包。这些包 的容量是相同的,都是 V。可以装进背包里的一共有 N 种物品,每种物品都有 给定的体积和价值。

在 DD 看来,合理的背包安排方案是这样的: 每个人背包里装的物品的总体积恰等于包的容量。 每个包里的每种物品最多只有一件,但两个不同的包中可以存在相同的物品。

任意两个人,他们包里的物品清单不能完全相同。 在满足以上要求的前提下,所有包里的所有物品的总价值最大是多少呢?

输入格式

第一行三个数K、V、N

接下来每行两个数,表示体积和价值

输出格式

前k优解的价值和

样例 #1

样例输入 #1

2 10 5
3 12
7 20
2 4
5 6
1 1

样例输出 #1

57

提示

对于100%的数据, K ≤ 50 , V ≤ 5000 , N ≤ 200 K\le 50,V\le 5000,N\le 200 K50,V5000,N200


思路:本题要求的是0/1背包的前k优解,状态定义dp[ i ][ j ][ k ]:前i个物品在容量为j的条件下第k优解的值,dp[ i ][ j ][ k ]=max(dp[ i-1 ][ j ][ p1 ],dp[ i-1 ][ j-v ][ p2 ]+m)。
如何解决要装满的这个问题?增加计数功能。
代码实现:

#include<iostream>
#include<cstring>
using namespace std;
#define MAX_K 50
#define MAX_V 5000
#define MAX_N 200
int dp[MAX_V+5][MAX_K+5];
int temp[MAX_K+5];
int main()
{
	int K,V,N;
	cin>>K>>V>>N; 
	dp[0][0]=1,dp[0][1]=0;
	for(int i=1,v,m;i<=N;i++)
	{
		cin>>v>>m;
		for(int j=V;j>=v;j--)
		{
			int p1=1,p2=1,t=1;
			int n1=dp[j][0],n2=dp[j-v][0];
			while(p1<=n1||p2<=n2)
			{
				if(p2>n2||(p1<=n1&&dp[j][p1]>=dp[j-v][p2]+m))
				temp[t++]=dp[j][p1++];
				else{
					temp[t++]=dp[j-v][p2++]+m;
				}
				if(t>K)break;
			}
			temp[0]=t-1;
			memcpy(dp[j],temp,sizeof(int)*t);
		}
	}
	int ans=0;
	for(int i=1;i<=K;i++)
	ans+=dp[V][i];
	cout<<ans;
	return 0;
 } 


14.仓库建设

题目描述

L 公司有 n n n 个工厂,由高到低分布在一座山上,工厂 1 1 1 在山顶,工厂 n n n 在山脚。

由于这座山处于高原内陆地区(干燥少雨),L公司一般把产品直接堆放在露天,以节省费用。突然有一天,L 公司的总裁 L 先生接到气象部门的电话,被告知三天之后将有一场暴雨,于是 L 先生决定紧急在某些工厂建立一些仓库以免产品被淋坏。

由于地形的不同,在不同工厂建立仓库的费用可能是不同的。第 i i i 个工厂目前已有成品 p i p_i pi 件,在第 i i i 个工厂位置建立仓库的费用是 c i c_i ci

对于没有建立仓库的工厂,其产品应被运往其他的仓库进行储藏,而由于 L 公司产品的对外销售处设置在山脚的工厂 n n n,故产品只能往山下运(即只能运往编号更大的工厂的仓库),当然运送产品也是需要费用的,一件产品运送一个单位距离的费用是 1 1 1

假设建立的仓库容量都都是足够大的,可以容下所有的产品。你将得到以下数据:

  • 工厂 i i i 距离工厂 1 1 1 的距离 x i x_i xi(其中 x 1 = 0 x_1=0 x1=0)。
  • 工厂 i i i 目前已有成品数量 p i p_i pi
  • 在工厂 i i i 建立仓库的费用 c i c_i ci

请你帮助 L 公司寻找一个仓库建设的方案,使得总的费用(建造费用 + 运输费用)最小。

输入格式

输入的第一行是一个整数 n n n,代表工厂的个数。

2 2 2 ( n + 1 ) (n + 1) (n+1) 行,每行有三个用空格隔开的整数,第 ( i + 1 ) (i + 1) (i+1) 行的整数依次代表 x i ,   p i ,   c i x_i,~p_i,~c_i xi, pi, ci

输出格式

仅输出一行一个整数,代表最优方案的费用。

样例 #1

样例输入 #1

3
0 5 10
5 3 100
9 6 10

样例输出 #1

32

提示

样例输入输出 1 1 1 解释

在工厂 1 1 1 和工厂 3 3 3 建立仓库,建立费用为 10 + 10 = 20 10+10=20 10+10=20 ,运输费用为 ( 9 − 5 ) × 3 = 12 (9-5) \times 3 = 12 (95)×3=12,总费用 32 32 32

数据范围与约定

对于 20 % 20\% 20% 的数据,保证 n ≤ 500 n \leq 500 n500

对于 40 % 40\% 40% 的数据,保证 n ≤ 1 0 4 n \leq 10^4 n104

对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 1 0 6 1 \leq n \leq 10^6 1n106 0 ≤ x i , p i , c i < 2 31 0 \leq x_i,p_i,c_i < 2^{31} 0xi,pi,ci<231

对于任意的 1 ≤ i < n 1 \leq i < n 1i<n,保证 x i < x i + 1 x_i < x_{i + 1} xi<xi+1

设答案为 a n s ans ans,保证 a n s + ∑ i = 1 n p i x i < 2 63 ans + \sum\limits_{i = 1}^{n} p_ix_i < 2^{63} ans+i=1npixi<263


思路:
本题状态定义dp[ i ]:前i个位置以i位置为最后一个仓库的总费用,
dp[ i ]=min(dp[ j ]+sum((xi-xk)*pk)+ci) (k的范围是j+1~i,ci为在第i个位置建设仓库的代价)。
本题可利用斜率优化来进行优化。
代码实现:

#include <iostream>
using namespace std;

#define MAX_N 1000000
long long dp[MAX_N + 5];
long long c[MAX_N + 5], x[MAX_N + 5];
long long s[MAX_N + 5]; // sum of pk
long long t[MAX_N + 5]; // sum of xk * pk
long long f[MAX_N + 5]; // f[i] = dp[i] + t[i]
long long q[MAX_N + 5], head = 0, tail = 0;

void set(long long i, long long j) {
    dp[i] = dp[j] + x[i] * (s[i] - s[j]) - (t[i] - t[j]) + c[i];
    f[i] = dp[i] + t[i];
    return ;
}

int main() {
    long long n;
    cin >> n;
    for (long long i = 1; i <= n; i++) {
        cin >> x[i] >> s[i] >> c[i];
        t[i] = t[i - 1] + x[i] * s[i];
        s[i] += s[i - 1];
    }
    set(1, 0);
    q[tail++] = 0;
    q[tail++] = 1;
    for (long long i = 2; i <= n; i++) {
        do {
            long long X = s[q[head + 1]] - s[q[head]];
            long long Y = f[q[head + 1]] - f[q[head]];
            if (tail - head >= 2 && Y < x[i] * X) ++head;
            else break;
        } while (1);
        set(i, q[head]);
        do {
            long long A = f[i] - f[q[tail - 1]];
            long long B = f[q[tail - 1]] - f[q[tail - 2]];
            long long C = s[i] - s[q[tail - 1]];
            long long D = s[q[tail - 1]] - s[q[tail - 2]];
            if (tail - head >= 2 && A * D < B * C) --tail;
            else break;
        } while (1);
        q[tail++] = i;
    }
    long long ans = dp[n];
    for (long long i = n - 1; i >= 1 && s[i] == s[i + 1]; i--) {
        ans = min(ans, dp[i]);
    }
    cout << ans << endl;
    return 0;
}
  • 35
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值