动态规划是常考算法,将指数级的问题降到多项式级别。
动态规划和分治法都是将问题划分成子问题进行求解,它们的区别主要是:
动态规划和贪心法的相同之处是原问题包含子问题的最优解,而它们的区别在于:
1. Weighted interval scheduling
带权重的最大兼容子集,不能用贪心法进行求解,需要考虑动态规划的解法。首先将活动按照结束时间排序。两种情况,一种是选当前的j,那么最优解等于从后往前找第一个与j不冲突的活动(使用二分查找加快速度)的opt加上j的权重,一种是不选j,那么最优解等于j的前一个活动的最优解。如下式:
o p t ( j ) = m a x { v j + o p t ( p ( j ) ) , o p t ( j − 1 ) } opt(j)=max\{v_j+opt(p(j)),opt(j-1)\} opt(j)=max{vj+opt(p(j)),opt(j−1)}
ps.若按照开始时间排序,要反过来找,那么opt(i)可以定义为,i~n个活动,p(i)是从前往后找第一个不冲突的活动。opt(n+1)=0,return opt(1)。
o p t ( i ) = m a x { v i + o p t ( p ( i ) ) , o p t ( i + 1 ) } opt(i)=max\{v_i+opt(p(i)),opt(i+1)\} opt(i)=max{vi+opt(p(i)),opt(i+1)}
2. Segmented least squares
线段拟合点集问题,希望线段尽量少,error尽量小。
error定义为E+cL ,E是平方误差之和,L是线段条数。
opt(j) 是 p 1 . . . p j p_1...p_j p1...pj的最小error,e(i,j)是 p i , p i + 1 . . . p j p_i,p_{i+1}...p_j pi,pi+1...pj的最小平方误差
o p t ( j ) = min 1 ≤ i ≤ j { e i j + c + o p t ( i − 1 ) } opt(j)=\min \limits_{1\leq i\leq j}\{e_{ij}+c+opt(i-1)\} opt(j)=1≤i≤jmin{eij+c+opt(i−1)}
这个算法的瓶颈在于计算 e i j e_{ij} eij,对j要扫一遍,j固定了,i要扫一遍,i固定了,在i j之间要扫一遍, 使得算法的效率是 O ( n 3 ) O(n^3) O(n3).
3. 买卖股票系列
5 | 2 | 7 | 13 | 1 | 9 |
---|---|---|---|---|---|
5 | 2 | 2 | 2 | 1 | 1 |
这道题还可以用分治法做,详见上一篇博文。
代码:
#include
using namespace std;
int sale[100005];
int t;
int dp1[100005];
int dp1_max[100005];
int dp2[100005];
int main(){
scanf("%d",&t);
while(t--){
int n;
scanf("%d",&n);
for(int i=0;i<n;i++){
scanf("%d",&sale[i]); //这里使用cin会超时
dp1[i]=0;
dp2[i]=0;
}
int res=0;
for(int i=1;i<n;i++){
dp1[i]=max(dp1[i-1]+sale[i]-sale[i-1],0);
dp1_max[i]=max(dp1_max[i-1],dp1[i]); //最大值存储别写错
dp2[i]=max(dp1_max[i-1],dp2[i-1]+sale[i]-sale[i-1]);
res=max(res,dp2[i]);
}
cout<<res<<endl;
}
return 0;
}
买卖k次
Best time to Buy and Sell Stock IV
以下解法借鉴自disscuss
public int maxProfit(int k, int[] prices) {
int n = prices.length;
if (n <= 1)
return 0;
//if k >= n/2, then you can make maximum number of transactions.
if (k >= n/2) {
int maxPro = 0;
for (int i = 1; i < n; i++) {
if (prices[i] > prices[i-1])
maxPro += prices[i] - prices[i-1];
}
return maxPro;
}
int[][] dp = new int[k+1][n];
for (int i = 1; i <= k; i++) {
int localMax = dp[i-1][0] - prices[0];
for (int j = 1; j < n; j++) {
dp[i][j] = Math.max(dp[i][j-1], prices[j] + localMax);
localMax = Math.max(localMax, dp[i-1][j] - prices[j]);
}
}
return dp[k][n-1];
}
public int maxProfit(int[] prices) {
//当前是买入状态(k<=i天买入,没卖出)
int[]hold=new int[prices.length+1];
int[]sold=new int[prices.length+1];//卖出状态
int[]rest=new int[prices.length+1];//冷静状态
hold[0]=Integer.MIN_VALUE;
sold[0]=0;
rest[0]=0;
for(int i=1;i<prices.length+1;i++){
//买前需要冷静
hold[i]=Math.max(hold[i-1],rest[i-1]-prices[i-1]);
//卖前需要买
sold[i]=hold[i-1]+prices[i-1];
//冷静期之前要么是休息要么是卖掉了
rest[i]=Math.max(rest[i-1],sold[i-1]);
}
return Math.max(rest[prices.length],sold[prices.length]);
}
以下是 θ ( 1 ) \theta(1) θ(1)空间解法和思路重现
Share my thinking process
public int maxProfit(int[] prices, int fee) {
if (prices.length <= 1) return 0;
int days = prices.length, buy[] = new int[days];
int sell[] = new int[days];
buy[0]=-prices[0]-fee;
for (int i = 1; i<days; i++) {
// keep the same as day i-1, or buy from sell status at day i-1
buy[i] = Math.max(buy[i - 1], sell[i - 1] - prices[i] - fee);
// keep the same as day i-1, or sell from buy status at day i-1
sell[i] = Math.max(sell[i - 1], buy[i - 1] + prices[i]);
}
return sell[days - 1];
}
4. 最长递增子序列
LeetCode Longest increasing subsequence
一个错误的思路
dp[i]=max{dp[p(i)]+1,dp[i-1]} 选i或者不选i,如果选,从后往前找到第一个小于i的值的dp值加1,这个思路不对的原因在于,没办法保证第一个小于i的值的状态是选了这个值的状态,也就是说,可能没有选这个p(i),它的当前最大值不能保证合法。
public int lengthOfLIS(int[] nums) {
if(nums.length==0) return 0;
int[]dp=new int[nums.length];
Arrays.fill(dp,1);
int result=1;
for(int i=1;i<nums.length;i++){
for(int j=i-1;j>=0;j--){
if(nums[j]<nums[i]){
dp[i]=Math.max(dp[i],dp[j]+1);
}
}
result=Math.max(result,dp[i]);
}
return result;
}
时间复杂度 θ ( n ) \theta(n) θ(n)
public int lengthOfLIS(int[] nums) {
int[] tails = new int[nums.length];
int size = 0;
for (int x : nums) {
int i = 0, j = size;
//求最接近且小于当前值的二分
while (i != j) {
int m = (i + j) / 2;
if (tails[m] < x)
i = m + 1;
else
j = m;
}
tails[i] = x;
if (i == size) ++size;
}
return size;
}
作业题
合唱队形
描述
N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学不交换位置就能排成合唱队形。
合唱队形是指这样的一种队形:设K位同学从左到右依次编号为1, 2, …, K,他们的身高分别为T1, T2, …, TK,则他们的身高满足T1 < T2 < … < Ti , Ti > Ti+1 > … > TK (1 <= i <= K)。
你的任务是,已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。
输入
输入的第一行是一个整数N(2 <= N <= 100),表示同学的总数。第一行有n个整数,用空格分隔,第i个整数Ti(130 <= Ti <= 230)是第i位同学的身高(厘米)。
输出
输出包括一行,这一行只包含一个整数,就是最少需要几位同学出列。
样例输入
8
186 186 150 200 160 130 197 220
样例输出
4
这道题是LIS的变种,其实就是维护两个最长递增子序列,后面那个递减的倒着来就是递增了。
#include
using namespace std;
int height[105];
int dp1[105];
int dp2[105];
int n;
int main(){
while(scanf("%d",&n)!=EOF) {
for (int i = 0; i < n; i++) {
cin >> height[i];
dp1[i] = 1;
dp2[i] = 1;
}
for (int i = 1; i < n; i++) {
for (int j = i - 1; j >= 0; j--) {
if (height[j] < height[i]) {
dp1[i] = max(dp1[i], dp1[j] + 1);
}
}
}
for (int i = n - 2; i >= 0; i--) {
for (int j = n - 1; j > i; j--) { //注意循环的边界
if (height[j] < height[i]) {
dp2[i] = max(dp2[i], dp2[j] + 1);
}
}
}
int result = 0;
for (int i = 0; i < n; i++) {
result = max(result, dp1[i] + dp2[i] - 1);
}
cout << n - result << endl;
}
return 0;
}
5. 子序列和最大系列
经典形式:连续子数组序列和最大
f ( j ) f(j) f(j)-以第j个元素结尾的最大和
f ( j ) = a j + m a x ( 0 , f ( j − 1 ) ) f(j)=a_j+max(0,f(j-1)) f(j)=aj+max(0,f(j−1))
return m a x { f ( j ) } max\{f(j)\} max{f(j)}
连续子数组和绝对值最大
求一个最大和,再求一个最小和,再取绝对值求最大
子序列乘积最大
因为存在负负得正的情况,需要维护一个最大乘积和一个最小乘积(可能是负值再乘一个负值就可能变成最大)
f m a x ( j ) = m a x { a j a j ∗ f m a x ( j − 1 ) a j ∗ f m i n ( j − 1 ) f_{max}(j)=max\left\{ \begin{aligned} a_j \\ a_j * f_{max}(j-1) \\ a_j*f_{min}(j-1) \end{aligned} \right. fmax(j)=max⎩⎪⎨⎪⎧ajaj∗fmax(j−1)aj∗fmin(j−1)
f m i n ( j ) = m i n { a j a j ∗ f m a x ( j − 1 ) a j ∗ f m i n ( j − 1 ) f_{min}(j)=min\left\{ \begin{aligned} a_j \\ a_j * f_{max}(j-1) \\ a_j*f_{min}(j-1) \end{aligned} \right. fmin(j)=min⎩⎪⎨⎪⎧ajaj∗fmax(j−1)aj∗fmin(j−1)
子序列和最大,但长度受限(不超过m)
naive 解法:j往前m个求最大 θ ( n m ) \theta(nm) θ(nm)
总体思路:
维护一个累加和数组, S j S_j Sj表示 a 1 + a 2 + . . . + a j a_1+a_2+...+a_j a1+a2+...+aj,我们需要得到的是最大的 S j − S i S_j-S_i Sj−Si,在j确定的情况下,只需要找到前m个中最小的 S i S_i Si
思路一:
用一个最小堆维护m个元素,维护长度为m的滑动窗口,每次删除窗口外最前的一个元素。
删除操作的实现:
①使用结构体struct {i,s[i]} ,在pop的时候发现i与j的差距超过m, continue θ ( n l o g n ) \theta(nlogn) θ(nlogn)
②使用支持删除任意元素的堆(STL库不支持) θ ( n l o g m ) \theta(nlogm) θ(nlogm)
思路2:
使用一个双向队列,size是m,最小值是队列的头元素,维护对应的index保证队列里面的数字和当前的index差m以内。队列中直接存index就行。
这个队列每次进来一个元素,pop掉所有比它大的元素。(类比:提拔干部时同等能力下选择年轻人,在这里元素越小能力越强,后进来的元素又年轻又强,则轮不到前面的老人,把它们删除。)
解法:https://blog.csdn.net/SSimpLe_Y/article/details/71792893
子矩阵和最大
描述
已知矩阵的大小定义为矩阵中所有元素的和。给定一个矩阵,你的任务是找到最大的非空(大小至少是1 * 1)子矩阵。
比如,如下4 * 4的矩阵
0 -2 -7 0
9 2 -6 2
-4 1 -4 1
-1 8 0 -2
的最大子矩阵是
9 2
-4 1
-1 8
这个子矩阵的大小是15。
输入
输入是一个N * N的矩阵。输入的第一行给出N (0 < N <= 100)。再后面的若干行中,依次(首先从左到右给出第一行的N个整数,再从左到右给出第二行的N个整数……)给出矩阵中的N2个整数,整数之间由空白字符分隔(空格或者空行)。已知矩阵中整数的范围都在[-127, 127]。
输出
输出最大子矩阵的大小。
样例输入
4
0 -2 -7 0 9 2 -6 2
-4 1 -4 1 -1
8 0 -2
样例输出
15
将二维问题转化为一维问题,假设行的范围确定,为i~j,那么横着将i到j的元素值相加即对应列元素的和,得到一组序列,再用连续子数组和最大来解。这组序列的长度也需要遍历,所以是 θ ( n 3 ) \theta(n^3) θ(n3)的时间复杂度。
#include
#include
#define INF 12700
using namespace std;
int nums[105][105];
int dp[105];
int main(){
int n;
while(scanf("%d",&n)!=EOF){
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
cin>>nums[i][j];
}
}
int result=-INF;
for(int i=0;i<n;i++){
memset(dp,0, sizeof(dp));
for (int j = i; j < n; ++j) {
int sum=0;
for(int k=0;k<n;k++){
dp[k]+=nums[j][k];
sum+=dp[k];
if(sum<0) sum=dp[k];
result=max(result,sum);
}
}
}
cout<<result<<endl;
}
return 0;
}
作业题 棋盘分割
描述
将一个8*8的棋盘进行如下分割:将原棋盘割下一块矩形棋盘并使剩下部分也是矩形,再将剩下的部分继续如此分割,这样割了(n-1)次后,连同最后剩下的矩形棋盘共有n块矩形棋盘。(每次切割都只能沿着棋盘格子的边进行)
原棋盘上每一格有一个分值,一块矩形棋盘的总分为其所含各格分值之和。现在需要把棋盘按上述规则分割成n块矩形棋盘,并使各矩形棋盘总分的均方差最小。
均方差,其中平均值,xi为第i块矩形棋盘的总分。
请编程对给出的棋盘及n,求出O’的最小值。
输入
第1行为一个整数n(1 < n < 15)。
第2行至第9行每行为8个小于100的非负整数,表示棋盘上相应格子的分值。每行相邻两数之间用一个空格分隔。
输出
仅一个数,为O’(四舍五入精确到小数点后三位)。
样例输入
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.633
把方差展开,由于棋盘总和的平均值一定, S 2 = 1 n ∑ ( X i − X ˉ ) 2 = 1 n ∑ X i 2 − 2 ∑ X i X ˉ + ∑ X ˉ 2 = 1 n ∑ X i 2 − X ˉ S^2=\frac{1}{n}\sum(X_i-\bar{X})^2=\frac{1}{n}\sum X_i^2-2\sum X_i\bar{X}+\sum\bar{X}^2=\frac{1}{n}\sum X_i^2-\bar{X} S2=n1∑(Xi−Xˉ)2=n1∑Xi2−2∑XiXˉ+∑Xˉ2=n1∑Xi2−Xˉ
则只需要求分割后棋盘的平方和最小就行了。维护一个累加和数组,sum[i][j]是以(0,0)为左上角,(i,j)为右下角的累加和。枚举竖着切和横着切的情况,用记忆数组存结果来剪枝。
#include
#include
#include
#include
using namespace std;
int nums[8][8];
int n;
int sum[8][8];
int memo[15][8][8][8][8];
int get_sum(int x1,int y1,int x2,int y2){
if(x1>0 && y1>0) {
return sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1];
}
if(x1>0){
return sum[x2][y2]-sum[x1-1][y2];
}
if(y1>0){
return sum[x2][y2]-sum[x2][y1-1];
}
return sum[x2][y2];
}
int cut(int n,int x1,int y1,int x2,int y2){
if(memo[n][x1][y1][x2][y2]!=-1) {
return memo[n][x1][y1][x2][y2];
}
int MIN=10000000;
if(n==1){
int a=get_sum(x1,y1,x2,y2);
memo[n][x1][y1][x2][y2]=a*a;
return a*a;
}
for(int i=x1;i<x2;i++){
int a=get_sum(i+1,y1,x2,y2);
int b=get_sum(x1,y1,i,y2);
int res=min(cut(n-1,x1,y1,i,y2)+a*a,cut(n-1,i+1,y1,x2,y2)+b*b);
MIN=min(MIN,res);
}
for(int i=y1;i<y2;i++){
int a=get_sum(x1,i+1,x2,y2);
int b=get_sum(x1,y1,x2,i);
int res=min(cut(n-1,x1,y1,x2,i)+a*a,cut(n-1,x1,i+1,x2,y2)+b*b);
MIN=min(MIN,res);
}
memo[n][x1][y1][x2][y2]=MIN;
return MIN;
}
int main(){
while(scanf("%d",&n)!=EOF){
memset(memo,-1, sizeof(memo));
memset(sum,0, sizeof(sum));
for(int i=0;i<8;i++){
int rowsum=0;
for(int j=0;j<8;j++){
cin>>nums[i][j];
rowsum+=nums[i][j];
if(i>0) {
sum[i][j] += sum[i - 1][j] + rowsum;
}
else{
sum[i][j]=rowsum;
}
// cout<
}
}
double res=n*cut(n,0,0,7,7)-sum[7][7]*sum[7][7];
cout<<setprecision(3)<<fixed<<sqrt(res/(n*n))<<endl;
}
return 0;
}
17年模拟考
#include
#include
#include
#include
using namespace std;
int b,l,n;
int w[1005];
int s[1005];
double res[1005];
int main(){
while(true){
cin>>b>>l>>n;
//cout<
if(b==0 && l==0 && n==0){break;}
l=l*60;
for(int i=1;i<=n;i++){
cin>>w[i]>>s[i];
}
res[0]=0.0;
for(int i=1;i<=n;i++){
int minv=s[i];
int load=w[i];
res[i]=1.0*l/s[i]+res[i-1]; //单独过桥
for(int j=i-1;j>=1;j--){
if(load+w[j]>b)break;
else{
minv=min(s[j],minv);//速度在减小
load+=w[j];
//如果第j辆车到第i辆车都可以安排在一起并且时间更少的话更新
if(res[i]>res[j-1]+1.0*l/minv){
res[i]=res[j-1]+1.0*l/minv;
}
}
}
}
cout<<fixed<<setprecision(1)<<res[n]<<endl;
}
//system("pause");
return 0;
}
#include
#include
#include
#include
using namespace std;
int n;
char str[5005];
int dp[5005][5005];
int main(){
cin>>n;
cin>>str;
memset(dp,0,sizeof(dp));
for(int i=n-1;i>=0;i--){
//for(int i=0;i
for(int j=i;j<n;j++){
if(str[i]==str[j]){
dp[i][j]=dp[i+1][j-1];
}
else{
dp[i][j]=min(1+dp[i+1][j],1+dp[i][j-1]);
}
}
}
cout<<dp[0][n-1]<<endl;
//system("pause");
return 0;
}
LeetCode
Cherry Pickup
Predict the winner
maximum length of repeated subarray