chap3.动态规划

动态规划的基本要素

重要性质:

最优子结构:包含子结构的最优解。自底向上,首先假设最优解导出的子问题解不是最优,再设法说明在这个假设下可构造出比原问题最优解更好的解,从而矛盾

重叠子问题:有的子问题被不断反复计算。

变形:

备忘录方法:自顶向下(和动态规划的区别),对每个子问题进行存储,部分子问题不用求解的时候,使用备忘录法有利

 步骤:

找出最优解的性质,并刻画其结构特征

递归地定义最优值

自底向上的方式计算最优值

根据计算最优值得到的信息构造最优解

矩阵连乘O(n3)

从小到大计算,每次将当前的直接与之前的相乘,然后开始拆分最优子结构进行组合找更优的组合。

#include<bits/stdc++.h>
using namespace std;
#define ull unsigned long long
#define ll long long
#define endl "\n"
#define debug freopen("C:/Users/HBH/Desktop/1.txt","w",stdout);
int n;
int m;
int f[1000][1000];
int s[1000][1000];
int p[1000];
void solve()
{
  for(int i=1;i<=n;i++)
  {
    f[i][i]=0;
  }
  for(int r=2;r<=n;r++)
  {
    for(int i=1;i<=n-r+1;i++)
    {
      int j=i+r-1;
      f[i][j]=f[i+1][j]+p[i-1]*p[i]*p[j];//先计算当前的
      s[i][j]=i;
      for(int k=i+1;k<j;k++)//找最优子结构
      {
        int t=f[i][k]+f[k+1][j]+p[i-1]*p[k]*p[j];
        if(t<f[i][j])
        {
          f[i][j]=t;
          s[i][j]=k;
        }
      }
    }
  }
}
void traceback(int i,int j)
{
  if(i==j)return;
  traceback(i,s[i][j]);
  traceback(s[i][j]+1,j);
  cout<<"multi A "<<i<<", "<<s[i][j];
  cout<<" and A "<<s[i][j]+1<<", "<<endl;
} 
int main()
{
  srand(time(0));
  cin >> n>>m;
  p[1]=rand()%10;
  for(int i=2;i<=n;i+=2)
  {
    p[i]=p[i+1]=rand()%10;
  }
  for(int i=1;i<=n;i+=1)
  {
    cout<<p[i]<<" ";
  }
  cout<<endl;
  solve();
  cout<<f[1][n];
  traceback(1,n);
}

 

最长公共子序列O(nm)

一共三种情况,

ai==bi,那么长度加一。

c[i-1][j]>=c[i][j-1]:a的前i-1个和b的前j个公共子序列长度大于a的前i个和b的前j-1个公共子序列长度,所以Xi和Yj的公共子序列和Xi-1 Yj的一样

c[i-1][j]<c[i][j-1]:a的前i-1个和b的前j个公共子序列长度小于a的前i个和b的前j-1个公共子序列长度,所以Xi和Yj的公共子序列和Xi Yj-1的一样

按照这三种情况标记,即可进行递归找出序列对应的数字

#include<bits/stdc++.h>
using namespace std;
#define ull unsigned long long
#define ll long long
#define endl "\n"
#define debug freopen("C:/Users/HBH/Desktop/1.txt","w",stdout);
int n;
int m;
int a[1000];//n
int b[1000];//m
int c[1000][1000];
int x[1000][1000];
void solve()
{
  for(int i=1;i<=n;i++)
  {
    c[i][0]=0;
  }
  for(int j=1;j<=m;j++)
  {
    c[0][j]=0;
  }
  for(int i=1;i<=n;i++)
  {
    for(int j=1;j<=m;j++)
    {
      if(a[i]==b[j])
      {
        c[i][j]=c[i-1][j-1]+1;
        x[i][j]=1;
      }
      else if(c[i-1][j]>=c[i][j-1])
      {
        c[i][j]=c[i-1][j];
        x[i][j]=2;
      }
      else
      {
        c[i][j]=c[i][j-1];
        x[i][j]=3;
      }
    }
  }
}
void out(int i,int j)
{
  if(i==0||j==0)return ;
  if(x[i][j]==1)
  {
    
    out(i-1,j-1);cout<<a[i]<<" ";
  }
  else if(x[i][j]==2)
  {
    out(i-1,j);
  }
  else out(i,j-1);
}
int main()
{
  srand(time(0));
  cin >> n>>m;
  for(int i=1;i<=n;i++)
  {
     a[i]=rand()%10;
     cout<<a[i]<<" ";
  }
  cout<<endl;
  for(int i=1;i<=m;i++)
  {
    b[i]=rand()%10;
    cout<<b[i]<<" ";
  }
  cout<<endl;

  solve();
  cout<<"maxn: "<<c[n][m]<<endl;
  out(n,m);
  
}

 

最大子段和O(n)

每次比较当前值,若大于累计和+当前值,则只用当前值,否则继续加。

#include<bits/stdc++.h>
using namespace std;
#define ull unsigned long long
#define ll long long
#define endl "\n"
#define debug freopen("C:/Users/HBH/Desktop/1.txt","w",stdout);
int n;
int s[10000];
int sum;
void solve()
{
  int now=0;
  for(int i=1;i<=n;i++)
  {
    if(now+s[i]<s[i])
    {
      now=s[i];
    }
    else now=now+s[i];
    sum=max(sum,now);
  }
}
int main()
{
  srand(time(0));
  cin >> n;
  for(int i=1;i<=n;i++)
  {
   int zhengfu=rand()%31;
   s[i]=rand()%10;
   if(zhengfu&1)s[i]=-s[i];
   cout<<s[i]<<" ";
  }
  cout<<endl;
  solve();
  cout<<sum<<endl;
  
}

延伸:最大子矩阵和O(n3)

和最大子段和差不多,只不过是把矩阵压成一行,相当于遍历每种压缩的可能性进行最大子段和操作

#include<bits/stdc++.h>
using namespace std;
#define ull unsigned long long
#define ll long long
#define endl "\n"
#define debug freopen("C:/Users/HBH/Desktop/1.txt","w",stdout);
int n;
int m;
int p[10000][10000];
int s[10000];
int maxnum()
{
  int sum=0;
  int now=0;
  for(int i=1;i<=n;i++)
  {
    if(now+s[i]<s[i])
    {
      now=s[i];
    }
    else now=now+s[i];
    sum=max(sum,now);
  }
  return sum;
}
void solve()
{
  int sum=0;
  for(int i=1;i<=n;i++)
  {
    for(int j=1;j<=m;j++)
    {
      s[j]=0;
    }
    for(int j=i;j<=n;j++)
    {
      for(int k=1;k<=m;k++)
      {
        s[k]+=p[j][k];
      }
      sum=max(sum,maxnum());
    }
  }
  cout<<sum<<endl;
}
int main()
{
  srand(time(0));
  cin >> n>>m;
  for(int i=1;i<=n;i++)
  {
    for(int j=1;j<=m;j++)
    {
      int zhengfu=rand()%31;
     p[i][j]=rand()%10;
     if(zhengfu&1)p[i][j]=-p[i][j];
    cout<<p[i][j]<<" ";
    }cout<<endl;
   
  }
  cout<<endl;
  solve();
  
  
}

最大m子段和问题

动态规划,借助矩阵可以直观的看到计算过程。
定义二维数组dp, dp[ i ][ j ],表示前 j 项所构成 i 子段的最大和,且必须包含着第j项,即以第j项结尾然后是一个递推过程。
求dp[ i ][ j ],有两种情况
1、dp[ i ][ j ] = dp[ i ] [ j-1 ] + a[ j ] ,即把第j项融合到第 j-1 项的子段中,子段数没变
2、dp[ i ][ j ] = dp[ i-1 ] [ t ] + a[ j ],(i-1<= t < j )
把第 j 项作为单独的一个子段,然后找一下i-1个子段时,最大的和,然后加上a[ j ] 然后比较上面两种情况,取大的。
下面看图,红色数字为输入的序列:

如图,要求dp[ 3 ][ 6 ],只需比较他左边的那个,和上面那一行圈起来的之中最大的数, 再加上a[ j ] 即为dp[ 3 ][ 6 ] 的值。
优化一下:
1、沿着第m行的最后一个元素,往左上方向画一条线,线右上方的元素是没必要计算的那么dp[ i ][ j ] ,j++的时候,j的上限为 i + n - m 即可。还有左下角那一半矩阵,也是不用计算的,因为1个数字不可能分为2个子段
2、每确定一个dp[ i ][ j ],只需用到本行和上一行,所以不用开维数组也可以,省内存。开两个一维数组,pre和dp,pre记录上一行,dp记录当前行
3、再对上一行红圈中的数字找最大值时,若用一个循环来找,容易超时。
优化方法:在每次计算dp之前,同时记录下j前面的最大元素。时间复杂度大致为O(m*(n-m+1)),mn-m方通过图片,分析情况1和2,就能发现,从左上角走到第 m 行的最后一个元素即可,找出第 m 行的最大值即为答案。

#include<bits/stdc++.h>
using namespace std;
#define ull unsigned long long
#define ll long long
#define endl "\n"
#define debug freopen("C:/Users/HBH/Desktop/1.txt","w",stdout);
int n;
int m;
int s[10000];
int c[10000];
int b[10000];
void solve()
{
  b[0]=0;
  c[1]=0;
  for(int i=1;i<=m;i++)
  {
    b[i]=b[i-1]+s[i];
    c[i-1]=b[i];
    int maxn=b[i];
    for(int j=i+1;j<=i+n-m;j++)
    {
      b[j]=b[j-1]>c[j-1]?b[j-1]+s[j]:c[j-1]+s[j];//b[j-1]:直接连 c[j-1]新开一段
      c[j-1]=maxn;//之前的最大选法方案
      maxn=max(maxn,b[j]);
    }
    c[i+n-m]=maxn;
  }
  int sum=0;
  for(int i=m;i<=n;i++)
  {
    if(sum<b[i])
    {
      sum=b[i];
    }
  }
  cout<<sum<<endl;
}
int main()
{
  srand(time(0));
  cin >> n>>m;
  for(int i=1;i<=n;i++)
  {
      int zhengfu=rand()%31;
     s[i]=rand()%10+1;
     if(zhengfu&1)s[i]=-s[i];
    cout<<s[i]<<" ";
  }
  cout<<endl;
  solve();
  
  
}

 

流水作业调度

n个作业{1,2,…,n},要在由机器M1和M2组成的流水线上完成加工。
每个作业加工的顺序都是先在M1上加工,然后在M2上加工。
M1和M2加工作业i所需的时间分别为ai和bi。
  要求确定这n个作业的最优加工顺序,使得从第一个作业在机器M1上开始加工,到最后一个作业在机器M2上加工完成所需的时间最少。

令N1={i ai<bi},N2={i ai>bi}

将N1中依ai递增排序,N2中按bi递减排序

然后N1+N2即为解

#include<bits/stdc++.h>
using namespace std;
#define ull unsigned long long
#define ll long long
#define endl "\n"
#define debug freopen("C:/Users/HBH/Desktop/1.txt","w",stdout);
struct node
{
  int key;
  int index;
  bool job;
  bool operator<(const node&a)const
  {
    return key<=a.key;
  }
};
int n;
int a[1000];
int b[1000];
int c[1000];
node p[1000];
int c1,c2;
  int head=1;
  int tail;
void solve()
{
  for(int i=1;i<=n;i++)
  {
    p[i].key=a[i]>b[i]?b[i]:a[i];
    p[i].job=a[i]<=b[i];
    p[i].index=i;
  }
  sort(p+1,p+1+n);
  tail=n;
  for(int i=1;i<=n;i++)
  {
    if(p[i].job)
    {
      c[head++]=p[i].index;
    }
    else c[tail--]=p[i].index;
  }
  c1=a[c[1]];
  c2=c1+b[c[1]];
  for(int i=2;i<=n;i++)
  {
    c1+=a[c[i]];
    c2=(c1>c2?c1:c2)+b[c[i]];
  }

}
int main()
{
  srand(time(0));
  cin >> n;
  for(int i=1;i<=n;i++)
  {
    a[i]=rand()%30;
    b[i]=rand()%30;
    cout<<a[i]<<" "<<b[i]<<endl;
  }
  solve();
  for(int i=1;i<=n;i++)
  {
    cout<<c[i]<<" ";
  }
  cout<<endl;
  cout<<"ans: "<<c2<<endl;
}

 

图像压缩

数据结构解释:

s[]:该段的位数

l[]:该段有的元素

b[]:该段每个元素存储位数

一段s[]:

如何推导?

对于每个s[i],从当前往前进行更新,每次更新分为两部分,前一半部分是s[k],后半部分是区间bmax,相加比较出最小值就是s[i]

 

 

#include<bits/stdc++.h>
using namespace std;
#define ull unsigned long long
#define ll long long
#define endl "\n"
#define debug freopen("C:/Users/HBH/Desktop/1.txt","w",stdout);
int s[1000];
int l[1000];
int b[1000];
int p[1000];
int n;
int len(int x)
{
  int le=1;
  x>>=1;
  while(x)
  {
    x>>=1;
    le++;
  }
  return le;
}

void solve()
{
  s[0]=0;
  int head=11;
  int lmax=256;
  for(int i=1;i<=n;i++)
  {

    b[i]=len(p[i]);//b为一个像素的位数
    int bmax=b[i];
    s[i]=s[i-1]+bmax;///初始化
    l[i]=1;
    for(int j=2;j<=i&&j<=lmax;j++)//根据前面来找最大值
    {
      if(b[i-j+1]>bmax)//找1~i-1中的b最大值
      {
        bmax=b[i-j+1];
      }
      if(s[i]>s[i-j]+j*bmax)//j为个数,bmax为长度,更新s[i]
      {
        s[i]=s[i-j]+j*bmax;
        l[i]=j;//更新l个数
      }
    }
    s[i]+=head;
  }
}
void print()
{
  for(int i=1;i<=n;i++)
  {
    cout<<i<<" s: "<<s[i]<<" l: "<<l[i]<<" b: "<<b[i]<<endl;
  }
}
void out()
{
  int pos=n;
  while(pos)
  {
    cout<<l[pos]<<" "<<b[pos]<<endl;
    pos=pos-l[pos];
  }
  cout<<endl;
}
int main()
{
  //ios::sync_with_stdio(false);
    //cin.tie(nullptr);
    while(1)
    {
      cin>>n;
    srand(time(0));
    for(int i=1;i<=n;i++)
    {
      p[i]=(rand())%256;
    }
    for(int i=1;i<=n;i++)
    {
      cout<<p[i]<<" ";
    }
    cout<<endl;
    solve();
    print();
    out();
    }
    
}

 0-1背包

这个是逆序写法,(书上是这样写的),但是感觉nm正序的写法更简单易懂一点

#include<bits/stdc++.h>
using namespace std;
#define ull unsigned long long
#define ll long long
#define endl "\n"
#define debug freopen("C:/Users/HBH/Desktop/1.txt","w",stdout);
int n;
int c;
int w[1000];
int v[1000];
int f[1000][1000];
int x[1000];
void solve()
{
  int jmax=min(w[n]-1,c);
  for(int i=0;i<=jmax;i++)
  {
    f[n][i]=0;
  }
  for(int i=w[n];i<=c;i++)//初始化
  {
    f[n][i]=v[n];
  }
  for(int i=n-1;i>1;i--)
  {
    jmax=min(w[i]-1,c);
    for(int j=0;j<=jmax;j++)//继承
    {
      f[i][j]=f[i+1][j];
    }
    for(int j=w[i];j<=c;j++)//更新
    {
      f[i][j]=max(f[i+1][j],f[i+1][j-w[i]]+v[i]);
    }
  }
  f[1][c]=f[2][c];
  if(c>=w[1])
    f[1][c]=max(f[1][c],f[2][c-w[1]]+v[1]);
  cout<<f[1][c]<<endl;
//输出结果
for(int i=1;i<n;i++) { if(f[i][c]==f[i+1][c]) { x[i]=0; } else { x[i]=1; c-=w[i]; } } x[n]=(f[n][c])?1:0; } int main() { srand(time(0)); cin >> n; for(int i=1;i<=n;i++) { w[i]=rand()%30; v[i]=rand()%30; c+=w[i]; cout<<w[i]<<" "<<v[i]<<endl; } c-=c/2-rand()%20; cout<<"c: "<<c<<endl; solve(); //cout<<f[1][c]<<endl; for(int i=1;i<=n;i++) { if(x[i]) cout<<i<<" "; } }

 

posted @ 2023-07-10 22:32  hbhhbh  阅读(43)  评论(0)    收藏  举报