动态规划 - DP

动态规划 - DP

(一)动态规划入门

一、动态规划入门

例题1:数字三角形 Luogu P1216

状态:\(f[i][j]\):第 \(i\) 行第 \(j\) 列上点到最后一行的最大和

状态转移:\(f[i][j] = max(f[i-1][j],f[i-1][j-1])+a[i][j]\)

for(int i=1;i<=n;i++)
    for(int j=1;j<=i;j++)
        f[i][j]=f[i][j]+max(f[i-1][j-1],f[i-1][j]);
sort(f[n]+1,f[n]+1+n);
printf("%d\n",f[n][n]);

例题2:公交乘车

一个特别的单行街道在每公里处有一个汽车站。顾客根据他们乘坐汽车的公里使来付费。

例如下表就是一个费用的单子。

公里数 1 2 3 4 5 6 7 8 9 10
价格 12 21 31 40 49 58 69 79 90 101

没有一辆车子行驶超过\(10\)公里,一个顾客打算行驶 \(n\) 公里(\(1<=n<=100\)

它可以通过无限次的换车来完成旅程。最后要求费用最少。

状态:\(f[i]\):前 \(i\) 公里的最小花费

状态转移:\(f[i] = min(f[i],f[i-j]+a[j])\)

for(int i=2;i<=n;i++)
{
    for(int j=1;j<=10;j++)
    {
        if(i-j<0) continue;
        f[i]=min(f[i],f[i-j]+a[j]);
    }
}

练习1:摘花生

Hello Kitty想摘点花生送给她喜欢的米老鼠。

她来到一片有网格状道路的矩形花生地(如下图),从西北角进去,东南角出来。

地里每个道路的交叉点上都有种着一株花生苗,上面有若干颗花生

经过一株花生苗就能摘走该它上面所有的花生。

Hello Kitty只能向东或向南走,不能向西或向北走。

问Hello Kitty最多能够摘到多少颗花生。

状态:\(f[i][j]\):从出发点到 \((i,j)\) 能够采花生的最大个数

状态转移:\(f[i][j]=max(f[i-1][j],f[i][j-1])+a[i][j]\)

for(int i=1;i<=r;i++)
    for(int j=1;j<=c;j++)
        f[i][j]=max(f[i-1][j],f[i][j-1])+a[i][j];
printf("%d\n",f[r][c]);

练习2:黑熊过河

有一只黑熊想过河,但河很宽,黑熊不会游泳,只能借助河面上的石墩跳过去。

它可以一次跳一墩,也可以一次跳两墩,

但是每跳一次都会耗费一定的能量,黑熊最终可能因能量不够而掉入水中。

所幸的是,有些石墩上放了一些食物,些食物可以给黑熊增加一定的能量。

问黑熊能否利用这些石墩安全地抵达对岸?请计算出抵达对岸后剩余能量的最大值。

状态:\(f[i]\):黑熊在第 \(i\) 个石头时拥有的最大能量

状态转移:\(f[i]=max(f[i-1],f[i-2])+a[i]-q\)

初值:\(f[0]=p,f[1]=p-q+a[1];\)

for(int i=2;i<=n+1;i++)
{
    if(f[i-1]-q<0&&f[i-2]-q<0)
    {
        printf("NO\n");
        return 0;
    }
    if(f[i-1]-q<0) f[i-1]=0;
    if(f[i-2]-q<0) f[i-2]=0;
    f[i]=max(f[i-1],f[i-2])-q+a[i];		
}
printf("%d\n",f[n+1]);

二、最长子序列问题 - LIS

例题1:LIS模板题

\(N\) 个整数,输出这 \(N\) 个整数的最长上升序列、

最长下降序列、最长不上升序列和最长不下降序列。

以最长上升序列为例:

状态:\(f[i]\):长度为 \(i\) 的上升子序列末尾元素的最小值

状态转移:\(j>i\)\(f[i] \geq a[j]\) 时,\(f[i] = a[j]\)

初值:\(f[i] = INF,f[1] = a[1]\)

for(int i=1;i<=n;i++)
    b[i]=a[i];
//上升 
for(int i=1;i<=n;i++)
    a[i]=b[i];
for(int i=1;i<=n+1;i++)
    f[i]=INF;
f[1]=a[1];
for(int i=2;i<=n;i++)
    *lower_bound(f+1,f+n+1,a[i])=a[i];
int ans1=lower_bound(f+1,f+2+n,INF)-(f+1);
//不下降 
for(int i=1;i<=n;i++)
    a[i]=b[i];
for(int i=1;i<=n+1;i++)
    f[i]=INF;
f[1]=a[1];
for(int i=2;i<=n;i++)
    *upper_bound(f+1,f+n+1,a[i])=a[i];
int ans2=lower_bound(f+1,f+2+n,INF)-(f+1);
//下降 
for(int i=1;i<=n;i++)
    a[i]=b[i]*-1;
for(int i=1;i<=n+1;i++)
    f[i]=INF;
for(int i=2;i<=n;i++)
    *lower_bound(f+1,f+n+1,a[i])=a[i];
int ans3=lower_bound(f+1,f+2+n,INF)-(f+1);
//不上升
for(int i=1;i<=n;i++)
    a[i]=b[i]*-1;
for(int i=1;i<=n+1;i++)
    f[i]=INF;
f[1]=a[1];
for(int i=2;i<=n;i++)
    *upper_bound(f+1,f+n+1,a[i])=a[i];
int ans4=lower_bound(f+1,f+2+n,INF)-(f+1);
printf("%d\n%d\n%d\n%d\n",ans1,ans3,ans4,ans2); 

例题2:导弹拦截 Luogu P1020 [NOIP1999 普及组]

根据题意,找出最长不上升和上升子序列即可。

for(int i=1;i<=n;i++)
	b[i]=a[i];

for(int i=1;i<=n;i++)
    a[i]=b[i]*-1;
for(int i=1;i<=n+1;i++)
    f[i]=INF;
f[1]=a[1];
for(int i=2;i<=n;i++)
    *upper_bound(f+1,f+n+1,a[i])=a[i];
int ans1=lower_bound(f+1,f+2+n,INF)-(f+1);

for(int i=1;i<=n;i++)
    a[i]=b[i];
for(int i=1;i<=n+1;i++)
    f[i]=INF;
f[1]=a[1];
for(int i=2;i<=n;i++)
    *lower_bound(f+1,f+n+1,a[i])=a[i];
int ans2=lower_bound(f+1,f+2+n,INF)-(f+1);

printf("%d\n%d\n",ans1,ans2);

例题3:Farmer_John收苹果

农场的夏季是收获的好季节。在Farmer John的农场,他们用一种特别的方式来收小苹果:

Bessie摇小苹果树,小苹果落下,然后Farmer John尽力接到尽可能多的小苹果。

作为一个有经验的农夫,Farmer John将这个过程坐标化。

他清楚地知道什么时候 \((1 \leq t \leq 1,000,000)\) 什么位置(用二维坐标表示,\(−1000 \leq x,y \leq 1000\))会有小苹果落下。

他只有提前到达那个位置,才能接到那个位置掉下的小苹果。

一个单位时间,Farmer John能走 \(s (1 \leq s \leq 1000)\) 个单位。

假设他开始时 \((t=0)\) 站在 \((0,0)\) 点,他最多能接到多少个小苹果?

Farmer John 在接小苹果时,从某个点到另外一点按照直线来走。

讲苹果按照落地时间排序,可得到如下状态:

状态:\(f[i]\):如果接第 \(i\) 个苹果,此时最多能接到的苹果个数

状态转移:\(f[i]=max(f[i],f[j]+1)\)

bool mycmp(Farmer_John a,Farmer_John b)
{
	return a.t<b.t;
}
double Sqrt(int i,int j)
{
	double k;
	k=sqrt((a[i].x-a[j].x)*(a[i].x-a[j].x)+(a[i].y-a[j].y)*(a[i].y-a[j].y));
	return k;
}
sort(a+1,a+1+n,mycmp);
for(int i=1;i<=n;i++)
    f[i]=-1;
f[0]=0;
for(int i=1;i<=n;i++)
{
    for(int j=0;j<i;j++)
    {
        if(f[j]>=0&&Sqrt(i,j)<=(a[i].t-a[j].t)*s)
            f[i]=max(f[i],f[j]+1);
    }
    ans=max(ans,f[i]);
}
printf("%d\n",ans);

练习1:合唱队形 Luogu P1091 [NOIP2004 提高组]

根据题意,可得如下状态:

状态:\(l[i]\):在区间 \([1 \sim i]\) 中的最长上升子序列

\(r[i]\):在区间 \([n \sim i]\) 中的最长上升子序列

状态转移:\(j<i\)\(a[i]>a[j]\) 时,\(l[i]=max(l[i],l[j]+1)\)

​ 当 \(j>i\)\(a[i]>a[j]\) 时,\(r[i]=max(r[i],r[j]+1)\)

for(int i=1;i<=n;i++)
    l[i]=r[i]=1;

for(int i=1;i<=n;i++)
    for(int j=1;j<i;j++)
        if(a[i]>a[j]) l[i]=max(l[i],l[j]+1);

for(int i=n;i>=1;i--) 
    for(int j=n;j>i;j--)
        if(a[i]>a[j]) r[i]=max(r[i],r[j]+1);

for(int i=1;i<=n;i++)
    minn=min(minn,n-l[i]-r[i]+1);
printf("%d\n",minn);

练习2:轮船问题

某国家被一条河划分为南北两部分,在南岸和北岸总共有 \(N\) 对城市

每一城市在对岸都有一个城市作为友好城市。

每一对友好城市都希望有一条航线来往,于是他们向政府提出了申请。

政府决定允许开通的航线就互不交叉(如果两条航线交叉,将有很大机会撞船)。

兴建哪些航线以使在安全条件下有最多航线可以被开通。

根据题意,将航线按照北岸的距离从小到大排序

此时南岸的距离所组成的序列中,找出最长上升子序列即可

sort(a+1,a+1+n,mycmp);
for(int i=1;i<=n+1;i++)
    dp[i]=INF;
dp[1]=a[1].d;
for(int i=2;i<=n;i++)
    *lower_bound(dp+1,dp+1+n,a[i].d)=a[i].d;
int ans=lower_bound(dp+1,dp+1+n,INF)-(dp+1);
printf("%d\n",ans);

练习3:护卫队 Luogu P1594

状态:\(f[i]\):前 \(i\) 辆车通过所需的最小时间

由于数组 \(f\)double 类型,min函数只能进行整形之间的比较大小

我们需要自己写一个 Min 函数进行两个 double 类型比较大小

double Min(double a,double b)
{
	if(a>b) return b;
	else return a;
}

状态转移:\(f[i]=min(f[i],f[j-1]+l/V)\)

初值:\(f[1]=l/s[1]\) (只有一辆车时花的时间肯定是桥的长度/这辆车的速度)

l*=60;//把每个s都除以60进行时间计算,不如直接将l乘以60
for(int i=1;i<=n;i++)
    f[i]=INF;
f[1]=l/s[1];
for(int i=2;i<=n;i++)
{
    double V=s[i];
    long long W=0;//W代表重量,即当前这一组的所有车的重量之和
    for(int j=i;j>=1;j--)
    {
        if(W+w[j]>v) break;
        W+=w[j];
        V=Min(V,s[j]);//每一组的速度取所有车中速度最小的一辆车的速度 
        f[i]=Min(f[i],f[j-1]+l/V);
    }
}
printf("%.1lf\n",f[n]);

(二)动态规划的要素和动机

例题1:音量调节 Luogu P1877 [HAOI2012]

状态:\(f[i][j]\):第 \(i\) 次修改之后,能否将音量改为 \(j\)

状态转移:\(f[i][j]=f[i][j]|f[i-1][j \pm a[i]]\)

f[0][beginlevel]=1;
for(int i=1;i<=n;i++)
{
    for(int j=maxlevel;j>=0;j--)
    {
        if(j-a[i]>=0) f[i][j]|=f[i-1][j-a[i]];
        if(j+a[i]<=maxlevel) f[i][j]|=f[i-1][j+a[i]];
    }
}
for(int i=maxlevel;i>=0;i--)
{
    if(f[n][i]) 
    {
        printf("%d\n",i);
        return 0;
    }
}
printf("-1\n");

例题2:Running S Luogu P1353 [USACO08JAN]

状态:\(f[i][j]\):第 \(i\) 秒疲劳度为 \(j\) 时能够跑的最远距离

状态转移:\(f[i][0]=f[i-1][0]\) 不动

\(f[i][0]=max(f[i][0],f[i-j][j])\) 休息

\(f[i][j]=f[i-1][j-1]+a[i]\) 跑步

for(int i=1;i<=n;i++)
{
    f[i][0]=f[i-1][0];//不动 
    for(int j=1;j<=m;j++)
    {
        if(i>=j) f[i][0]=max(f[i][0],f[i-j][j]);//休息 
        f[i][j]=f[i-1][j-1]+a[i];//跑步 
    }
}
printf("%d\n",f[n][0]);

例题3:饥饿的奶牛 Luogu P1868

Tips: 此处提供做法仅适用于 \(n \leq 2000\),对于原题的大数据版应该如何做,此处暂不做解释。

状态:\(f[i]\):前 \(i\) 个食槽中,最多有多少个食槽能被不交叉的线段覆盖,且线段的右端点不能大于第 \(i\) 个食槽

状态转移:\(f[i]=max(f[i],f[i-line[j].num]+line[j].num)\)

for(int i=1;i<=m;i++)
{
    f[i]=f[i-1];
    for(int j=1;j<=b;j++)
        if(line[j].right==i)
            f[i]=max(f[i],f[i-line[j].num]+line[j].num);
} 
printf("%d\n",f[m]);

例题4:尼克的任务 Luogu P1280

状态:\(f[i]\):前 \(i\) 分钟的最大空余时间

本题和上题不同之处在于,如果有任务到来时,必须要选择

而选择与否又会对后面任务产生影响,不满足动态规划的无后效性

正难则反,因此可以倒着递推,当某个点为多条线段的左端点时

这些线段对后面的影响已经求出,即可解决本题

状态:\(f[i]\):后 \(i\) 分钟的最大空余时间

状态转移:\(f[i]=max(f[i],f[i+line[k].num])\) 某条线段的左端点

\(f[i]=f[i+1]+1\) 不是某条线段的左端点

先将各个任务按照左端点排序

bool mycmp(node a,node b)
{
	return a.left<b.left;
}
sort(line+1,line+1+k,mycmp);
for(int i=n;i>=1;i--)
{
    if(line[k].left!=i)	f[i]=f[i+1]+1;
    else 
    {
        while(line[k].left==i)
        {
            f[i]=max(f[i],f[i+line[k].num]);
            k--;
        }
    }
}
printf("%d\n",f[1]);

练习1:Dollar Dayz S Luogu P6205 [USACO06JAN]

练习2:石头剪刀布(暂无题面)

练习3:Cow Frisbee Team S Luogu P2946 [USACO09MAR]

(三)资源分配类动态规划

例题1:机器分配 Luogu P2066

Tips: 此处只解答第一问 “最大盈利值”,具体方案暂不做解释。

状态:\(f[i][j]\):前 \(i\) 个公司总共得到了 \(j\) 台机器所获得的最优值

状态转移:\(f[i][j]=max(f[i-1][j],f[i-1][j-k]+w[i][k])\)

for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++)
        for(int k=1;k<=j;k++)
            f[i][j]=max(f[i-1][j],f[i-1][j-k]+w[i][k]);
printf("%d\n",f[n][m]);

例题2:书的复制 Luogu P1281

状态:\(f[i][j]\):前 \(i\) 个人抄写前 \(j\) 本书所用的最长时间

状态转移:\(f[i][j]=min(f[i][j],max(f[i-1][l],sum[j]-sum[l]))\)

for(int i=1;i<=m;i++)
	f[1][i]=sum[i];
for(int i=2;i<=k;i++)
    for(int j=1;j<=m;j++)
        f[i][j]=MAXN;
for(int i=2;i<=k;i++)
    for(int j=1;j<=m;j++)
        for(int l=1;l<j;l++)
            f[i][j]=min(f[i][j],max(f[i-1][l],sum[j]-suiym[l]));

对于输出,需要特殊处理一个 \(ans\) 数组来存储答案。

int head=0;
for(int i=m;i>=1;i--)
{
    int x=0;ans[++head].b=i;
    while(x+a[i]<=f[k][m])
    {
        x+=a[i];
        i--;
        if(i==0) break;
    }
    i++;
    ans[head].a=i;
}
for(int i=head;i>=1;i--)
    printf("%d %d\n",ans[i].a,ans[i].b);

练习1:马棚

每天,小明和他的马外出,然后他们一边跑一边玩耍。

当他们结束的时候,必须带所有的马返回马棚,小明有 \(k\) 个马棚。

他把他的马排成一排然后跟随它走向马棚,

因为他们非常疲劳,小明不想让他的马做过多的移动。因此他想了一个办法:

将马按照顺序放在马棚中,后面的马放的马棚的序号不会小于前面的马放的马棚的序号。

而且,他不想他的K个马棚中任何一个空置,也不想任何一匹马在外面。

已知共有黑、白两种马,而且它们相处得并不十分融洽。

如果有 \(i\) 个白马和 \(j\) 个黑马在一个马棚中,那么这个马棚的不愉快系数将是 \(i\times j\)

所有 \(k\) 个马棚不愉快系数的和就是系数总和。

确定一种方法把 \(n\) 匹马放入 \(k\)个马棚,使得系数总和最小。

练习2:花店橱窗布置 Luogu P1854

练习3:雇佣计划

一位管理项目的经理想要确定每个月需要的工人,他当然知道每月所需的最少工人数。

当他雇佣或解雇一个工人时,会有一些额外支出。

一旦一个工人被雇佣,即使他不工作,他也将得到工资。

这位经理知道雇佣一个工人的费用,解雇一个工人的费用和一个工人的工资。

现他在考虑一个问题:为了把项目的费用控制在最低,他将每月雇佣或解雇多少个工人。

(四)背包问题

一、01背包

例题1:采药 Luogu P1048 [NOIP2005 普及组]

状态:\(f[i][j]\):前 \(i\) 个物品放入一个容量为 \(j\) 的背包可以获得的最大值

状态转移:\(f[i][j] = max(f[i-1][j],f[i-1][c-v[i]]+w[i])\)

我们发现,第 \(f[i][j]\) 只与 \(f[i-1][j]\)\(f[i-1][j-v[i]]\) 有关

因此可以倒序枚举 \(j\),省略第一维状态。

for(int i=1;i<=m;i++)
{
    for(int j=1;j<=t;j++)
    {
        f[i][j]=f[i-1][j];
        if(j-v[i]>=0) f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
    }
}
for(int i=1;i<=t;i++)
    ans=max(ans,f[m][i]);
printf("%d\n",ans);

例题2:集合求和

对于从 \(1\)\(N (1 \leq N \leq 39)\) 的连续整数集合,

能划分成两个子集合,且保证每个集合的数字和是相等的。

举个例子,如果 \(N=3\)

对于 \(\{1,2,3\}\) 能划分成两个子集合,他们每个的所有数字和是相等的:

\(\{3\}\)\(\{1,2\}\)

这是唯一一种分法(交换集合位置被认为是同一种划分方案,因此不会增加划分方案总数)

如果 \(N=7\)

有四种方法能划分集合 \(\{1,2,3,4,5,6,7\}\),每一种分法的子集合各数字和是相等的:

\(\{1,6,7\}\)\(\{2,3,4,5\}\)

\(\{2,5,7\}\)\(\{1,3,4,6\}\)

\(\{3,4,7\}\)\(\{1,2,5,6\}\)

\(\{1,2,4,7\}\)\(\{3,5,6\}\)

给出 \(N\),你的程序应该输出划分方案总数,

如果不存在这样的划分方案,则输出 \(0\)

根据题意,当 \(n\) 个数的和为奇数时,输出 \(0\)

sum=(1+n)*n/2;
if(sum%2==1)
{
    cout<<0;
    return 0;
}

状态:\(f[i][j]\):前 \(i\) 个数,累加和为 \(j\) 的方案数

状态转移:\(f[i][j]=f[i-1][j]+f[i-1][j-i]\)

sum/=2;
f[0]=1;
int ans=0;
for(int i=1;i<=n;i++)
    for(int j=sum;j>=i;j--)
        f[j]=f[j]+f[j-i];
printf("%d\n",f[sum]\2);

练习1:开心的金明 Luogu P1060 [NOIP2006 普及组]

练习2:最大约数和 Luogu P1734

练习3:笔直的水管

奶牛们想把水从池塘运输到牛棚里,池塘和牛棚相距 \(D\) 个单位。

它们有 \(P\) 根水管,每根水管由 \(2\) 个整数来描述:水管长度 \(L_i\),最大流量 \(C_i\)

水管可以依次连接构成一条运输管道,

那么这条运输管道的流量就是构成这条管道的所有水管中最小的一个流量。

但是,要让水从池塘通过运输管道流到牛棚里,

管道的长度必须恰好等于池塘和牛棚的距离(也就是说,水管长度 \(L_i\) 之和为 \(D\))!

在只要求构造一条运输管道,求其最大流量。

二、完全背包

例题1:完全背包

有一个负重能力为 \(m(m \leq 300)\) 的背包和 \(n(n \leq 0)\) 种物品,

\(i\) 种物品的价值为 \(v[i]\),重量为 \(w[i]\)

在不超过背包负重能力的前提下选择若干个物品装入背包,使这些物品的价值之和最大。

每种物品可以不选,也可以选择多个。假设每种物品都有足够的数量。

我们可以将其转换为 01背包。

状态:\(f[i][j]\):前 \(i\) 种物品放入到容量为 \(j\) 的背包的最大权值

状态转移:\(f[i][j]=max(f[i][j],f[i-1][j-k \times v[i]]+k \times w[i]), \ 0 \leq k \times v[i] \leq c\)

我们在做 01背包 时倒着枚举第二层循环是为了保证物品只用 \(1\)

而完全背包并没有这个限制,每一个物品可以无限次使用

只需要把 01背包 的第二层循环改为正序枚举即可

for(int i=1;i<=n;i++)
    for(int j=0;j<=m;j++)
        if(j-w[i]>=0) f[j]=max(f[j],f[j-w[i]]+v[i]);
printf("max=%d\n",f[m]);

例题2:质数和分解

任何大于 \(1\) 的自然数 \(n\) 都可以写成若干个大于等于 \(2\) 且小于等于 \(n\) 的质数之和表达式

(包括只有一个数构成的和表达式的情况),并且可能有不止一种质数和的形式。

例如,\(9\) 的质数和表达式就有四种本质不同的形式:

\(9 = 2 + 5 + 2 = 2 + 3 + 2 + 2 = 3 + 3 + 3 = 2 + 7\)

这里所谓两个本质相同的表达式是指:

可以通过交换其中一个表达式中参加和运算的各个数的位置而直接得到另一个表达式。

试编程求解自然数 \(n\) 可以写成多少种本质不同的质数和表达式。

先进行质数筛,用 \(vis\) 数组标记 \(i\) 是否为质数。

状态:\(f[i][j]\):前 \(i\) 个质数累加和为 \(j\) 的方案数

状态转移:\(f[j]=f[j]+f[j-i]\)

f[0]=1;
for(int i=1;i<=n;i++)
{
    if(vis[i]) continue;
    for(int j=2;j<=n;j++)
        if(j-i>=0) f[j]=f[j]+f[j-i];
}
printf("%d\n",f[n]);

练习:上课

新学期开始了,小Y就要选课上了。

小Y所在的学校有一个奇怪的上课系统,有 \(N\) 种课可以选择,

每种课可以重复的上,并且每次上都花掉同样的时间,获得同样的知识量。

小Y同学每天有一定的上课时间总额,他想获得最大的知识量,

你能告诉他最多能得到多少知识吗?

三、多重背包

例题:逃亡的准备

在《Harry Potter and the Deathly Hallows》中,Harry Potter他们一起逃亡,

现在有许多的东西要放到赫敏的包里面,但是包的大小有限,

所以我们只能够在里面放入非常重要的物品,现在给出该种物品的数量、体积、价值的数值

希望你能够算出怎样能使背包的价值最大的组合方式,

并且输出这个数值,赫敏会非常地感谢你。

状态:\(f[i][j]\):前 \(i\) 种物品放入容量为 \(j\) 的背包的最大权值

状态转移:\(f[i][j]=max(f[i][j],f[i-1][j-k \times v[i]]+k \times w[i]),\ 0 \leq k \leq a[i]\)

如果单独枚举 \(k\),效率较低,容易 TLE,因此可以进行二进制拆分,大幅减少枚举次数。

for(int i=1;i<=n;i++)
{
    if(a[i]*v[i]>V) //如果物品总体积大于背包体积,转换为完全背包
    {
        for(int j=0;j<=V;j++)
            if(j>=v[i]) f[j]=max(f[j],f[j-v[i]]+w[i]);
    }
    else
    {
        int k=1,rest=a[i]; //k为二进制控制出来的数,rest为剩下的
        while(k<rest)
        {
            for(int j=V;j>=0;j--)
                if(j-k*v[i]>=0) f[j]=max(f[j],f[j-k*v[i]]+k*w[i]);
            rest-=k;
            k+=k;
        }
        for(int j=V;j>=0;j--)
            if(j-rest*v[i]>0) f[j]=max(f[j],f[j-rest*v[i]]+rest*w[i]);
    }
}
printf("%d\n",f[v]);

练习:最少硬币问题

设有 \(n\) 种不同面值的硬币,各硬币的面值存于数组 \(T[1 \sim n]\) 中。现要

用这些面值的硬币来找钱。可以使用的各种面值的硬币个数存于数组 $Coins [1 \sim n] $ 中。

对任意钱数 \(0 \leq m \leq 20001\),设计一个用最少硬币找钱 \(m\) 的方法。

对于给定的 \(1 \leq n \leq 10\),硬币面值数组 \(T\) 和可以使用的各种面值的硬币个数数组 \(Coins\)

以及钱数 \(m\)\(0 \leq m \leq 20001\),编程计算找钱 \(m\) 的最少硬币数。

四、多维费用背包

例题:NASA的食物计划 Luogu P1507

按照 01背包 的表示方法,加一维进行表示即可。

状态:\(f[i][j][k]\):前 \(i\) 个物品付出代价分别为 \(c\)\(u\) 时可以获得的最大价值

状态转移:\(f[i][j][k]=max(f[i][j][k],f[i-1][j-v[i]][k-m[i]]+w[i])\)

for(int k=1;k<=n;k++)
{
    for(int i=V;i>=1;i--)
    {
        for(int j=M;j>=1;j--)
        {
            if(i-v[k]>=0&&j-m[k]>=0)
                f[i][j]=max(f[i][j],f[i-v[k]][j-m[k]]+c[k]);
        }
    }
}
printf("%d\n",f[V][M]);

练习:潜水员的规划

潜水员为了潜水要使用特殊的装备。他有一个带 \(2\) 种气体的气缸:一个为氧气,一个为氮气。

让潜水员下潜的深度需要各种的数量的氧和氮。

潜水员有一定数量的气缸,每个气缸都有重量和气体容量。

潜水员为了完成他的工作需要特定数量的氧和氮。

他完成工作所需气缸的总重的最低限度的是多少?

例如:潜水员有 \(5\) 个气缸。每行三个数字为:氧,氮的(升)量和气缸的重量:

\(3 \ 36 \ 120\)

\(10 \ 25 \ 129\)

\(5 \ 50 \ 250\)

\(1 \ 45 \ 130\)

\(4 \ 20 \ 119\)

如果潜水员需要 \(5\) 升的氧和 \(60\) 升的氮则总重最小为 \(249\)\(1,2\) 或者 \(4,5\) 号气缸)。

你的任务就是计算潜水员为了完成他的工作需要的气缸的重量的最低值。

五、分组背包

例题:竞赛真理

根据每一题解题时间的估计值,确定一种做题方案

即哪些题目认真做,哪些题目 “骗” 分,哪些不做,使能在限定的时间内获得最高的得分。

状态:\(f[k][j]\):前 \(k\) 组物品花费 \(j\) 时可以获得的最大价值

状态转移:\(f[k][j]=max(f[k-1][j],f[k-1][j-w[i]]+v[i])\)

按照 01背包,第一维可以省略。

for(int k=1;k<=n;k++)
{
    for(int c=t;c>=0;c--)
    {
        if(c-t1[k]>=0) f[c]=max(f[c],f[c-t1[k]]+w1[k]);
        if(c-t2[k]>=0) f[c]=max(f[c],f[c-t2[k]]+w2[k]);
    }
}
printf("%d\n",f[t]);

(五)区间动态规划

例题1:石子合并(弱化版) Luogu P1775

状态:\(f[i][j]\):从第 \(i\) 堆石子到第 \(j\) 堆石子,合并成为一堆石子的最小得分

状态转移:\(f[i][j]=f[i][k]+f[k+1][j]+sum[j]-sum[i-1]\)

for(int p=1;p<=n;p++)
{
    for(int i=1;i<=n;i++)
    {
        int j=i+p-1;
        if(j>n) break;
        for(int k=i;k<j;k++)
            if(f[i][j]>f[i][k]+f[k+1][j]+sum[j]-sum[i-1]||f[i][j]==0)
                f[i][j]=f[i][k]+f[k+1][j]+sum[j]-sum[i-1];
    }
}
printf("%d\n",f[1][n]); 

例题2:环形石子合并

\(n\) 堆石子绕圆形操场排放,现要将石子有序地合并成一堆。

规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。

请编写一个程序,读入堆数 \(n\) 及每堆的石子数,并进行如下计算:

  1. 选择一种合并石子的方案,使得做 \(n−1\) 次合并得分总和最大。
  2. 选择一种合并石子的方案,使得做 \(n−1\) 次合并得分总和最小。

与上题相同,仅需把环转换成链即可。

例题3:能量项链 Luogu P1063 [NOIP2006 提高组]

状态:\(f[i][j]\):从 \(i\)\(j\) 的最大释放能量

状态转移:\(f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+a[i]*a[k+1]*a[j+1])\)

for(int p=1;p<=2*n;p++)
{
    for(int i=1;i<=2*n-p+1;i++)
    {
        int j=i+p-1;
        if(j>2*n) break;
        for(int k=i;k<j;k++)
            f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+a[i]*a[k+1]*a[j+1]);
    }
}
for(int i=1;i<=n;i++)
    ans=max(ans,f[i][i+n-1]);
printf("%d\n",ans); 

例题4:关灯

宁智贤得到了一份有趣而高薪的工作。每天早晨她必须关掉她所在村庄的街灯。

所有的街灯都被设置在一条直路的同一侧。

宁智贤每晚到早晨5点钟都在晚会上,然后她开始关灯。

开始时,她站在某一盏路灯的旁边,每盏灯都有一个给定功率的电灯泡

因为宁智贤有着自觉的节能意识,她希望在耗能总数最少的情况下将所有的灯关掉。

宁智贤因为太累了,所以只能以 \(1m / s\)的速度行走。

关灯不需要花费额外的时间,因为当她通过时就能将灯关掉。

计算在给定路灯设置,灯泡功率以及宁智贤的起始位置的情况下,

关掉所有的灯需耗费的最小能量。

状态:\(f[i][j]\):从第 \(i\) 号灯到第 \(j\) 号灯全部关闭的最小能量

而对于这个状态来说,人将这个区间的灯全部关闭之后

只有处于这个区间两端的时候,才有可能是最优方案,所以,得到如下状态:

状态:\(f[i][j][0]\):关闭区间 \([i,j]\) 中的灯之后,人在左端点的最小能量

\(f[i][j][1]\):关闭区间 \([i,j]\) 中的灯之后,人在右端点的最小能量

预处理:\(p[i][j]\):关闭区间 \([i,j]\) 中的的灯之后,其他灯的单位时间消耗

for(int i=1;i<=n;i++)
    w[i]+=w[i-1];
for(int i=1;i<=n;i++)
    for(int j=i;j<=n;j++)
        p[i][j]=w[n]+w[i-1]-w[j];

状态转移:见代码

for(int i=0;i<=n;i++)
    for(int j=0;j<=n;j++)
        f[i][j][0]=f[i][j][1]=INF;
f[v][v][0]=f[v][v][1]=0;
for(int l=2;l<=n;l++)
{
    for(int i=1;i<=n-l+1;i++)
    {
        int j=i+l-1;
        if(f[i][j][0]>f[i+1][j][0]+(d[i+1]-d[i])*p[i+1][j])
            f[i][j][0]=f[i+1][j][0]+(d[i+1]-d[i])*p[i+1][j];
        if(f[i][j][0]>f[i+1][j][1]+(d[j]-d[i])*p[i+1][j])
            f[i][j][0]=f[i+1][j][1]+(d[j]-d[i])*p[i+1][j];
        if(f[i][j][1]>f[i][j-1][1]+(d[j]-d[j-1])*p[i][j-1])
            f[i][j][1]=f[i][j-1][1]+(d[j]-d[j-1])*p[i][j-1];
        if(f[i][j][1]>f[i][j-1][0]+(d[j]-d[i])*p[i][j-1])
            f[i][j][1]=f[i][j-1][0]+(d[j]-d[i])*p[i][j-1];
    }
}
if(f[1][n][0]>f[1][n][1]) printf("%d\n",f[1][n][1]);
else printf("%d\n",f[1][n][0]);

练习1:矩阵连乘

给定 \(n\) 个矩阵 \(\{ A_1, A_2, \cdots , A_n \}\),考察这 \(n\) 个矩阵的连乘积$ A_1A_2 \cdots An$ 。

由于矩阵乘法满足结合律,故计算矩阵的连乘积可以有许多不同的计算次序,

这种计算次序可以用加括号的方式来确定。矩阵连乘积的计算次序与其计算量有密切关系。

例如,考察计算 \(3\) 个矩阵$ { A_1, A_2, A_3 }$ 连乘积的例子。

设这3个矩阵的维数分别为 \(10 \times 100\)\(100 \times 5\)\(5 \times 50\)

若按 \((A1A2)A3\) 计算,\(3\) 个矩阵连乘共需要 \(10 \times 100 \times 5+10 \times 5 \times 50=7500\) 次数乘。

若按 \(A1(A2A3)\) 计算,则总共需要 \(100 \times 5 \times 50+10 \times 100 \times 50=75000\) 次数乘。

现在你的任务是给出一个矩阵连乘式,计算其需要的最少乘法次数。

练习2:矩阵取数游戏 Luogu P1005 [NOIP2007 提高组]

练习3:AtCoder Express 2 AtCoder ABC106D

练习4:玩具取名 Luogu P4290 [HAOI2008]

练习5:监狱

小X的王国中有一个奇怪的监狱,这个监狱一共有 \(P\) 个牢房,这些牢房一字排开,

\(i\) 个仅挨着第 \(i+1\) 个(最后一个除外),当然第 \(i\) 个也挨着第 \(i−1\) 个(第一个除外),

现在牢房正好是满员的。上级下发了一个释放名单,要求每天释放名单上的一个人。

这可把看守们吓得不轻,因为看守们知道,现在牢房里的 \(P\) 个人,

可以相互之间传话。第 \(i\) 个人可以把话传给第 \(i+1\) 个,当然也能传给第 \(i−1\) 个,

并且犯人很乐意把消息传递下去。

如果某个人离开了,那么原来和这个人能说上话的人,都会很气愤,

导致他们那天会一直大吼大叫,搞得看守很头疼。

如果给这些要发火的人吃上肉,他们就会安静下来。

为了河蟹社会,现在看守们想知道,如何安排释放的顺序,才能是的他们消耗的肉钱最少。

练习6:游戏 A Game Luogu P2734 [USACO3.3]

(六)双进程类动态规划

例题1:最长公共子序列

字符序列的子序列是指:

从给定字符序列中随意地(不一定连续)去掉若干个字符,

也可能一个也不去掉,去掉后所形成的字符序列为 字符序列的子序列。

令给定的字符序列\(X=\{x_0,x_1,\cdots ,x_{m-1}\}\),序列\(Y=\{y_0,y_1,\cdots ,y_{k-1}\}\)\(X\) 的子序列,

存在 \(X\) 的一个严格递增下标序列 \(\{i_0,i_1,\cdots,i_{k-1}\}\)

使得对所有的\(j=0,1,\cdots,k-1\) ,有 \(x_{ij}=y_j\)

例如,\(X=\verb!"ABCBDAB"!\)\(Y=\verb!"BCDB"!\)\(X\) 的一个子序列。

对给定的两个字符序列,求出他们最长的公共子序列长度。

状态:\(f[i][j]\)\(X\) 的前 \(i\) 位和 \(Y\) 的前 \(j\) 位所构成的最长公共子序列的长度

状态转移:\(f[i][j]=max(f[i][j-1],f[i-1][j])\)

for(int i=1;i<=len1;i++)
{
    for(int j=1;j<=len2;j++)
    {
        f[i][j]=max(f[i][j-1],f[i-1][j]);
        if(s1[i]==s2[j]&&f[i][j]<=f[i-1][j-1]) f[i][j]++;
        ans=max(ans,f[i][j]);
    }
}
printf("%d\n",ans);

例题2:配置魔药

魔药课,Snape要他们每人配置一种魔药(不一定是一样的),现在Harry面前有两个坩埚,

有许多种药材要放进坩埚里,但坩埚的能力有限,无法同时配置所有的药材。

一个坩埚相同时间内只能加工一种药材,但是不一定每一种药材都要加进坩埚里。

加工每种药材都有必须在一个起始时间和结束时间内完成

(起始时间所在的那一刻和结束时间所在的那一刻也算在完成时间内)

每种药材都有一个加工后的药效,现在要求的就是Harry可以得到最大的药效。

状态:\(f[i][j][k]\):前 \(i\) 种草药,第一个坩埚用时 \(j\),第二个坩埚用时 \(k\) 能得到的最大功效

状态转移:\(j \geq a[i].t2\) 时,\(f[j][k]=max(f[j][k],f[a[i].t1-1][k]+a[i].w)\)

​ 当 \(k \geq a[i].t2\) 时,\(if(k>=a[i].t2) f[j][k]=max(f[j][k],f[j][a[i].t1-1]+a[i].w)\)

bool mycmp(node a,node b)
{
	return a.t2<b.t2;
}
for(int i=1;i<=n;i++)
    cin>>a[i].t1>>a[i].t2>>a[i].w;
sort(a+1,a+1+n,mycmp);
for(int i=1;i<=n;i++)
{
    for(int j=t;j>=0;j--)
    { 
        for(int k=t;k>=0;k--)
        {
            if(j>=a[i].t2) f[j][k]=max(f[j][k],f[a[i].t1-1][k]+a[i].w);
            if(k>=a[i].t2) f[j][k]=max(f[j][k],f[j][a[i].t1-1]+a[i].w);
        }
    }
}
printf("%d\n",f[t][t]);

练习1:编辑距离 Luogu P2758

状态:\(f[i][j]\):字符串 \(A\) 的前 \(i\) 个字符变为字符串 \(B\) 的前 \(j\) 个需要的最少步数

状态转移:
\(delete:f[i][j]=min(f[i][j],f[i-1][j]+1);\)
\(input:f[i][j]=min(f[i][j],f[i][j-1]+1);\)
\(change:f[i][j]=min(f[i][j],f[i-1][j-1]+1);\)
\(none:f[i][j]=min(f[i][j],f[i-1][j-1]);\)

for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++)
        f[i][j]=INF;
for(int i=0;i<=n;i++)
    f[i][0]=i;
for(int i=0;i<=m;i++)
    f[0][i]=i;
for(int i=1;i<=n;i++)
{
    for(int j=1;j<=m;j++)
    {
        //delete  ABCD ABC
        f[i][j]=min(f[i][j],f[i-1][j]+1);
        //input  ABC ABCD
        f[i][j]=min(f[i][j],f[i][j-1]+1);
        //change  ABCE ABCD
        if(s1[i]!=s2[j]) f[i][j]=min(f[i][j],f[i-1][j-1]+1);
        //none  ABCD ABCD
        else f[i][j]=min(f[i][j],f[i-1][j-1]);
    }
}
cout<<f[n][m]<<endl;

练习2:交错匹配

有两行自然数,\(up[1 \sim n]\)\(down[1 \sim n]\),如果 \(up[i]=down[j]=k\)

那么上行的第 \(i\) 个位置的数就可以跟下行的第 \(j\) 个位置的数连一条线,称为一条 \(K\) 匹配,

但是同一个位置的数最多只能连一条线。

另外,每个 \(k\) 匹配都必须且至多跟一个 \(l\) 匹配相交且 \(k \neq l\) 。现在要求一个最大的匹配数。

例如:以下两行数的最大匹配数为 \(8\)

(七)二维动态规划

例题1:过河卒 Luogu P1002 [NOIP2002 普及组]

\(vis\) 数组标记马能到达的坐标

状态:\(f[i][j]\):到达点 \((i,j)\) 的路径数目

状态转移:\(f[i][j]=f[i-1][j]+f[i][j-1]\)

for(int i=0;i<=yb;i++)
    if(vis[0][i]) break;
	else f[0][i]=1;
for(int i=0;i<=xb;i++)
    if(vis[i][0]) break;
	else f[i][0]=1;
for(int i=1;i<=xb;i++)
{
    for(int j=1;j<=yb;j++)
    {
        if(vis[i][j]) f[i][j]=0;
        else f[i][j]=f[i-1][j]+f[i][j-1];
    }
}
printf("%d\n",f[xb][yb]);

例题2:传纸条 Luogu P1006 [NOIP2008 提高组]

状态:\(f[i][j][k][l]\):第一个纸条传到了 \((i,j)\),第二个纸条传到了 \((k,l)\) 的时候能获得的最优值

我们发现,四维状态会枚举许多不会出现的状态

当两个纸条同步的时候,在某个时间,他们所处的两个位置行列之和是相等的

因为他们走的步数是一样的,由此,得到如下状态:

状态:\(f[p][i][j]\):当前走了 \(p\) 步,第一个个人走到第 \(i\) 行,第二个人走到第 \(j\) 行的最大价值

状态转移:

\[f[k][i][j]=max(max(f[k-1][i][j],f[k-1][i-1][j]),\\ max(f[k-1][i][j-1],f[k-1][i-1][j-1]))+a[i][k-(i-1)]+a[j][k-(j-1)]; \]

for(int k=1;k<=m+n-1;k++)
{
    for(int i=1;i<=m;i++)
    {
        for(int j=i+1;j<=m;j++)
        {
            if(i>k||j>k) continue;
            f[k][i][j]=max(max(f[k-1][i][j],f[k-1][i-1][j]),max(f[k-1][i][j-1],f[k-1][i-1][j-1]))+a[i][k-(i-1)]+a[j][k-(j-1)];			
        }
    }
}
printf("%d\n",max(f[n+m-1][m][m-1],f[n+m-1][m-1][m]));

例题3:农田个数

你的老家在农村。过年时,你回老家去拜年。

你家有一片 \(N \times M\) 农田,将其看成一个 \(N \times M\) 的方格矩阵,有些方格是一片水域。

你的农村伯伯听说你是学计算机的,给你出了一道题:

他问你:这片农田总共包含了多少个不存在水域的正方形农田。

两个正方形农田不同必须至少包含下面的两个条件中的一条:

  1. 边长不相等
  2. 左上角的方格不是同一方格

状态:\(f[i][j]\):以方格 \((i,j)\) 为右下角,可以得到的最大无正方形边长

状态:\(f[i][j]=min(f[i-1][j],min(f[i][j-1],f[i-1][j-1]))+1\)

for(int i=1;i<=n;i++)
{
    for(int j=1;j<=m;j++)
    {
        if(a[i][j]==0) f[i][j]=0;
        else f[i][j]=min(f[i-1][j],min(f[i][j-1],f[i-1][j-1]))+1;
        ans+=f[i][j];
    }
}
printf("%d\n",ans);

练习1:矩阵切割

给你一个矩阵,其边长均为整数。

你想把矩阵切割成总数最少的正方形,其边长也为整数。

切割工作由一台切割机器完成,

它能沿平行于矩形任一边的方向,从一边开始一直切割到另一边。

对得到的矩形再分别进行切割。

练习2:创意吃鱼法 Luogu P1736

练习3:方格取数 Luogu P1004 [NOIP2000 提高组]

练习4:方格取数 Luogu P7074 [CSP-J2020]

练习5:滑雪 Luogu P1434 [SHOI2002]

posted @ 2022-07-26 22:49  Lan_Sky  阅读(433)  评论(0)    收藏  举报