最佳加法表达式 DP

    xiaoxiao2025-08-21  8

    题意: 有一个由1..9组成的数字串.问如果将m个加号插入到这个数字串中,在各种可能形成的表达式中,值最小的那个表达式的值是多少。 输入: 5 3 1 2 3 4 5 输出: 24

    核心内容总结:所谓的DP是指 从最小的子结构最优,推往全局的过程。

                              跟是不是递推或者dfs还是记忆化无半毛钱关系。

                              重剑无锋大巧不工,贵在领悟精神。

    今天写这篇主要是为了总体的认识一下DP。

    DP是什么呢:说白了就是一个找最优子结构的过程至于愿怎么找,那就随意了。   

                              但说白了核心还是通过遍历(暴力,怎么暴力怎么来)来找各个最优子结构,由最小的局部最优推至全局。

    其实拿for循环,循环套循环和dfs都一样,其实都是从最局部最小的问题最优,推至全局最优。

    综上总结:1.递推(自底向上法)  2.递归(带备忘的自顶向下法) 其实都是一种东东。

     

    这道题比较典型,就拿这道题为例吧!

    在10个数字中放任意个加号使得组成的表达式的和最小。 状态转移方程: 将m个加号插入到n个数字组成的数字串中 V(m,n) 表示将m个加号插入到n个数字组成的数字串中组成的表达式和最小的表达式 i表示在第i个数后面插入加号 V(m,n) = Min{ V(m-1,i) + Num(i+1,n) } ( i = m … n-1) 表达式的最后一个加号添加在第 i 个数字后面 这个i要试遍所有的情况

     

    1.先写一下裸跑dfs,由于不用记忆化,新手的话比较容易理解。

       坏处是不开记忆化单纯费时间,因为暴搜的过程中产生了许多冗余的搜索。

       好处是不开的话,省下了记忆各个最优子结构的内存。

       具体取舍,开还是不开根据实际情况来定。

    #include<iostream> #include<cstdio> #include<algorithm> using namespace std; const int INF = 99999999; int a[1005],num[1005][1005]; int V(int m,int n) { if(m == 0)//无加号 return num[1][n]; else if(n < m+1)//加号过多 return INF; else { int t = INF; //这里有点像回溯,回溯的话递归里面有循环 //说说实话,递归真的很简单,就是把递推公式写进来 for(int i = m;i <= n-1;i++) //这里是n减一,排除了最后一个数字后面放加号的情况 t = min(t, V(m-1,i)+num[i+1][n]); //不懂的位置画个实例,会很简单,因为熟悉,所以容易 return t; } } int main() { int n,m; while(~scanf("%d%d",&n,&m)) { for(int i = 1;i <= n;i++) scanf("%d",&a[i]); //预处理,计算i到j数字串组成的数字 for(int i = 1;i <= n;i++) { num[i][i] = a[i];//只有一个数字 for(int j = i+1;j <= n;j++) { //这个表达式也可以好好看一下 num[i][j] = num[i][j-1]*10 + a[j]; } } cout<< V(m,n) <<endl; } return 0; }

    2.记忆化搜索

    int V(int m,int n) { if(v[m][n]!=0)return v[m][n]; for(int i = m;i <= n-1;i++) t = min(t, V(m-1,i)+num[i+1][n]); return v[m][n]=t; } 既然已经知道当前子结构的最优情况,爆破再遇见时就不用再扫一遍了。 所以因为少扫了很多东西,所以很省时间。 但也得开内存去存各个子结构的最优情况!!! #include<iostream> #include<cstdio> #include<algorithm> using namespace std; const int INF = 99999999; int a[1005],num[1005][1005]; int v[1005][1005]; int V(int m,int n) { int t = INF; if(v[m][n]!=0)return v[m][n]; if(m == 0)//无加号 return num[1][n]; else if(n < m+1)//加号过多 return INF; else { //这里有点像回溯,回溯的话递归里面有循环 //说说实话,递归真的很简单,就是把递推公式写进来 for(int i = m;i <= n-1;i++) //这里是n减一,排除了最后一个数字后面放加号的情况 t = min(t, V(m-1,i)+num[i+1][n]); //不懂的位置画个实例,会很简单,因为熟悉,所以容易 } return v[m][n]=t; } int main() { int n,m; while(~scanf("%d%d",&n,&m)) { for(int i = 1;i <= n;i++) scanf("%d",&a[i]); //预处理,计算i到j数字串组成的数字 for(int i = 1;i <= n;i++) { num[i][i] = a[i];//只有一个数字 for(int j = i+1;j <= n;j++) { //这个表达式也可以好好看一下 num[i][j] = num[i][j-1]*10 + a[j]; } } cout<< V(m,n) <<endl; } return 0; }

    3.自下而上的递推 也就是一般而言的DP

    留意一下,确实是 i=1,j=i,k=i开始扫的 利用for循环遍历所有情况,而且是自底向上,通过最小单位的子结构 来推往全局的。 for(int i=1;i<=m;i++) for(int j=i;j<=n;j++) for(int k=i;k<=j;k++) #include<iostream> #include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int INF=0x3f3f3f3f; const int N=1005; int a[N],num[N][N],dp[N][N]; //a[N]里面是存数字串 //num[i][j]表示数字串a[N]的第i位到第j位之间的数字串表示的数组 //dp[i][j]在i个数字中插入j个加号所能形成的表达式最小值 int main(){ int n,m; while(scanf("%d %d",&n,&m)){ for(int i=1;i<=n;i++){ scanf("%d",&a[i]); } //预处理,计算i到j数字串组成的数字 for(int i=1;i<=n;i++){ num[i][i]=a[i];//只有一个数字 for(int j=i+1;j<=n;j++){ num[i][j]=num[i][j-1]*10+a[j]; } } memset(dp,0x3f,sizeof(dp)); for(int i=1;i<=n;i++){ dp[0][i]=num[1][i];//无加号时 } //其实就是感觉在那个位置放不放加号 //这里n可以写在m前面。要加一个限制条件n>m,好麻烦,所以m在前且n=m+1 //这里k的取值范围就是m到n,k表示在第k个数后面插入加号 for(int i=1;i<=m;i++) for(int j=i;j<=n;j++) for(int k=i;k<=j;k++) dp[i][j]=min(dp[i][j],dp[i-1][k]+num[k+1][j]); cout<<dp[m][n]<<endl; } }

     

    最新回复(0)