区间dp算法刷题笔记【蓝桥杯】

  • Post author:
  • Post category:其他




理论

区间



d

p

dp






d


p





是一种动态规划算法,用于解决区间问题。它的基本思想是将问题分解成若干子问题,然后通过递推求解整个问题。

下面是一个经典的区间



d

p

dp






d


p





问题:

给定一个长度为



n

n






n





的序列



a

a






a





,求



a

a






a





的一个子区间



[

l

,

r

]

[l,r]






[


l


,




r


]





,使得区间和最大。

我们可以定义一个状态



f

[

i

]

[

j

]

f[i][j]






f


[


i


]


[


j


]





表示区间



[

i

,

j

]

[i,j]






[


i


,




j


]





的最大和,然后通过状态转移方程来求解最优解。

状态转移方程为:



f

[

i

]

[

j

]

=

m

a

x

(

f

[

i

+

1

]

[

j

]

,

f

[

i

]

[

j

1

]

)

+

a

[

i

]

+

a

[

j

]

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






f


[


i


]


[


j


]




=








ma


x


(


f


[


i




+








1


]


[


j


]


,




f


[


i


]


[


j













1


])




+








a


[


i


]




+








a


[


j


]






其中,



a

[

i

]

a[i]






a


[


i


]





表示序列a中第i个元素的值。这个方程的意义是,我们在区间



[

i

,

j

]

[i,j]






[


i


,




j


]





中选择一个端点,然后把这个端点前面或后面的区间作为子区间,使得区间和最大。

代码实现如下:

int n; // 序列的长度
int a[MAXN]; // 序列a
int f[MAXN][MAXN]; // 状态数组
int solve() {
    for (int len = 2; len <= n; len++) { // 枚举区间长度
        for (int i = 1; i + len - 1 <= n; i++) { // 枚举区间起点
            int j = i + len - 1; // 区间终点
            f[i][j] = max(f[i+1][j], f[i][j-1]) + a[i] + a[j];
        }
    }
    return f[1][n]; // 返回整个序列的最大子区间和
}

在上面的代码中,我们先枚举区间长度



l

e

n

len






l


e


n





,然后再枚举区间起点



i

i






i





,计算出区间



[

i

,

j

]

[i,j]






[


i


,




j


]





的最大和。最终,我们返回整个序列的最大子区间和



f

[

1

]

[

n

]

f[1][n]






f


[


1


]


[


n


]







练习



关路灯

某一村庄在一条路线上安装了



n

n






n





盏路灯,每盏灯的功率有大有小(即同一段时间内消耗的电量有多有少)。老张就住在这条路中间某一路灯旁,他有一项工作就是每天早上天亮时一盏一盏地关掉这些路灯。

为了给村里节省电费,老张记录下了每盏路灯的位置和功率,他每次关灯时也都是尽快地去关,但是老张不知道怎样去关灯才能够最节省电。他每天都是在天亮时首先关掉自己所处位置的路灯,然后可以向左也可以向右去关灯。开始他以为先算一下左边路灯的总功率再算一下右边路灯的总功率,然后选择先关掉功率大的一边,再回过头来关掉另一边的路灯,而事实并非如此,因为在关的过程中适当地调头有可能会更省一些。

现在已知老张走的速度为



1

m

/

s

1m/s






1


m


/


s





,每个路灯的位置(是一个整数,即距路线起点的距离,单位:



m

m






m





)、功率(



W

W






W





),老张关灯所用的时间很短而可以忽略不计。

请你为老张编一程序来安排关灯的顺序,使从老张开始关灯时刻算起所有灯消耗电最少(灯关掉后便不再消耗电了)。


输入格式

第一行是两个数字



n

n






n





(表示路灯的总数)和



c

c






c





(老张所处位置的路灯号);

接下来



n

n






n





行,每行两个数据,表示第



1

1






1





盏到第



n

n






n





盏路灯的位置和功率。数据保证路灯位置单调递增。


输出格式

一个数据,即最少的功耗(单位:



J

J






J









1

J

=

1

W

×

s

1J=1W\times s






1


J




=








1


W




×








s





)。


样例输入 #1

5 3
2 10
3 20
5 20
6 30
8 10


样例输出 #1

270


提示


样例解释

此时关灯顺序为

3 4 2 1 5


数据范围




1

n

50

1\le n\le50






1













n













50









1

c

n

1\le c\le n






1













c













n






思路

  • 比较典型的区间dp问题
  • 观察发现关灯的顺序是从中心向两边扩展的区间,设



    [

    0

    /

    1

    ]

    [0/1]






    [


    0/1


    ]





    表示把



    [

    i

    ,

    j

    ]

    [i,j]






    [


    i


    ,




    j


    ]





    区间内灯关掉,结束时人在最左/最右时的最少耗电,有转移方程:




    f

    [

    i

    ]

    [

    j

    ]

    [

    0

    ]

    =

    m

    i

    n

    (

    f

    [

    i

    +

    1

    ]

    [

    j

    ]

    [

    0

    ]

    +

    d

    (

    i

    ,

    i

    +

    1

    )

    P

    ,

    f

    [

    i

    +

    1

    ]

    [

    j

    ]

    [

    1

    ]

    +

    d

    (

    i

    ,

    j

    )

    P

    )

    f[i][j][0]=min(f[i+1][j][0]+d(i,i+1)*P,f[i+1][j][1]+d(i,j)*P)






    f


    [


    i


    ]


    [


    j


    ]


    [


    0


    ]




    =








    min


    (


    f


    [


    i




    +








    1


    ]


    [


    j


    ]


    [


    0


    ]




    +








    d


    (


    i


    ,




    i




    +








    1


    )













    P


    ,




    f


    [


    i




    +








    1


    ]


    [


    j


    ]


    [


    1


    ]




    +








    d


    (


    i


    ,




    j


    )













    P


    )









    f

    [

    i

    ]

    [

    j

    ]

    [

    1

    ]

    =

    m

    i

    n

    (

    f

    [

    i

    ]

    [

    j

    1

    ]

    [

    0

    ]

    +

    d

    (

    i

    ,

    j

    )

    P

    ,

    f

    [

    i

    ]

    [

    j

    1

    ]

    [

    1

    ]

    +

    d

    (

    j

    1

    ,

    j

    )

    P

    )

    f[i][j][1]=min(f[i][j-1][0]+d(i,j)*P,f[i][j-1][1]+d(j-1,j)*P)






    f


    [


    i


    ]


    [


    j


    ]


    [


    1


    ]




    =








    min


    (


    f


    [


    i


    ]


    [


    j













    1


    ]


    [


    0


    ]




    +








    d


    (


    i


    ,




    j


    )













    P


    ,




    f


    [


    i


    ]


    [


    j













    1


    ]


    [


    1


    ]




    +








    d


    (


    j













    1


    ,




    j


    )













    P


    )







  • d

    d






    d





    为距离,



    P

    P






    P





    可以用前缀和

    第一个式子,因为i+1在数轴上在i的右边,所以是i是由i+1转移而来


题解

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 55;
const ll INF = 0x3f3f3f3f3f3f3f3fll;
ll f[N][N][2];
int P[N], L[N], n, c, pre[N];
int main(){
	scanf("%d%d", &n, &c);
	for(int i = 1; i <= n; ++i){
		scanf("%d%d", L+i, P+i);
		pre[i] = pre[i-1] + P[i];
	}
	for(int l = 1; l <= n; ++l){
		for(int i = 1, j; (j = i + l - 1) <= n; ++i){
			if(i <= c && c <= j){
				f[i][j][0] =
					min(
						f[i+1][j][0] + (L[i+1] - L[i]) * (pre[n] - pre[j] + pre[i]), 
						f[i+1][j][1] + (L[j] - L[i]) * (pre[n] - pre[j] + pre[i])
					);
				f[i][j][1] =min(f[i][j-1][0] + (L[j] - L[i]) * (pre[n] - pre[j-1] + pre[i-1]), f[i][j-1][1] + (L[j] - L[j-1]) * (pre[n] - pre[j-1] + pre[i-1]));
			}else f[i][j][0] = f[i][j][1] = INF;
		}
	}
	printf("%lld\n", min(f[1][n][0], f[1][n][1]));
}



[CQOI2007]涂色

假设你有一条长度为



5

5






5





的木板,初始时没有涂过任何颜色。你希望把它的



5

5






5





个单位长度分别涂上红、绿、蓝、绿、红色,用一个长度为



5

5






5





的字符串表示这个目标:



RGBGR

\texttt{RGBGR}







RGBGR






每次你可以把一段连续的木板涂成一个给定的颜色,后涂的颜色覆盖先涂的颜色。例如第一次把木板涂成



RRRRR

\texttt{RRRRR}







RRRRR






,第二次涂成



RGGGR

\texttt{RGGGR}







RGGGR






,第三次涂成



RGBGR

\texttt{RGBGR}







RGBGR






,达到目标。

用尽量少的涂色次数达到目标。


输入格式

输入仅一行,包含一个长度为



n

n






n





的字符串,即涂色目标。字符串中的每个字符都是一个大写字母,不同的字母代表不同颜色,相同的字母代表相同颜色。


输出格式

仅一行,包含一个数,即最少的涂色次数。


样例输入 #1

AAAAA


样例输出 #1

1


样例输入 #2

RGBGR


样例输出 #2

3


提示




40

%

40\%






40%





的数据满足



1

n

10

1\le n\le 10






1













n













10








100

%

100\%






100%





的数据满足



1

n

50

1\le n\le 50






1













n













50






思路

  • 状态:



    f

    [

    i

    ]

    [

    j

    ]

    f[i][j]






    f


    [


    i


    ]


    [


    j


    ]





    表示区间



    [

    i

    ,

    j

    ]

    [i,j]






    [


    i


    ,




    j


    ]





    的最少操作次数

  • 状态转移方程:

    • 如果区间左右端点目标颜色相同,则可以免费涂最左或最右的一块:



      f

      [

      i

      ]

      [

      j

      ]

      =

      m

      i

      n

      (

      f

      [

      i

      +

      1

      ]

      [

      j

      ]

      ,

      f

      [

      i

      ]

      [

      j

      +

      1

      ]

      )

      f[i][j]=min(f[i+1][j],f[i][j+1])






      f


      [


      i


      ]


      [


      j


      ]




      =








      min


      (


      f


      [


      i




      +








      1


      ]


      [


      j


      ]


      ,




      f


      [


      i


      ]


      [


      j




      +








      1


      ])




    • 否则,从中间分成两个区间转移:



      f

      [

      i

      ]

      [

      j

      ]

      =

      m

      i

      n

      (

      f

      [

      i

      ]

      [

      k

      ]

      ,

      f

      [

      k

      +

      1

      ]

      [

      j

      ]

      )

      f[i][j]=min(f[i][k],f[k+1][j])






      f


      [


      i


      ]


      [


      j


      ]




      =








      min


      (


      f


      [


      i


      ]


      [


      k


      ]


      ,




      f


      [


      k




      +








      1


      ]


      [


      j


      ])





题解

#include<bits/stdc++.h>
using namespace std;
char s[55];
int f[55][55];
int main(){
    scanf("%s",s);
    int n=strlen(s);
    memset(f,0x3f,sizeof(f));
    for(int i=0;i<n;++i) f[i][i]=1;
    for(int l=2;l<=n;++l){
        for(int i=0,j;(j=i+l-1)<n;++i){
            if(s[i]==s[j]) f[i][j]=min(f[i][j],min(f[i+1][j],f[i][j-1]));
            else{
                for(int k=i;k<j;++k)f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]); 
            }
        }
    }
    printf("%d",f[0][n-1]);
}



[HNOI2010]合唱队

为了在即将到来的晚会上有更好的演出效果,作为 AAA 合唱队负责人的小 A 需要将合唱队的人根据他们的身高排出一个队形。假定合唱队一共



n

n






n





个人,第



i

i






i





个人的身高为



h

i

h_i







h










i





















米(



1000

h

i

2000

1000 \le h_i \le 2000






1000














h










i





























2000





),并已知任何两个人的身高都不同。假定最终排出的队形是



A

A






A





个人站成一排,为了简化问题,小 A 想出了如下排队的方式:他让所有的人先按任意顺序站成一个初始队形,然后从左到右按以下原则依次将每个人插入最终棑排出的队形中:

  • 第一个人直接插入空的当前队形中。

  • 对从第二个人开始的每个人,如果他比前面那个人高(



    h

    h






    h





    较大),那么将他插入当前队形的最右边。如果他比前面那个人矮(



    h

    h






    h





    较小),那么将他插入当前队形的最左边。





n

n






n





个人全部插入当前队形后便获得最终排出的队形。

例如,有



6

6






6





个人站成一个初始队形,身高依次为



1850

,

1900

,

1700

,

1650

,

1800

,

1750

1850, 1900, 1700, 1650, 1800, 1750






1850


,




1900


,




1700


,




1650


,




1800


,




1750







那么小 A 会按以下步骤获得最终排出的队形:




  • 1850

    1850






    1850








  • 1850

    ,

    1900

    1850, 1900






    1850


    ,




    1900





    ,因为



    1900

    >

    1850

    1900 > 1850






    1900




    >








    1850








  • 1700

    ,

    1850

    ,

    1900

    1700, 1850, 1900






    1700


    ,




    1850


    ,




    1900





    ,因为



    1700

    <

    1900

    1700 < 1900






    1700




    <








    1900








  • 1650

    ,

    1700

    ,

    1850

    ,

    1900

    1650, 1700, 1850, 1900






    1650


    ,




    1700


    ,




    1850


    ,




    1900





    ,因为



    1650

    <

    1700

    1650 < 1700






    1650




    <








    1700








  • 1650

    ,

    1700

    ,

    1850

    ,

    1900

    ,

    1800

    1650, 1700, 1850, 1900, 1800






    1650


    ,




    1700


    ,




    1850


    ,




    1900


    ,




    1800





    ,因为



    1800

    >

    1650

    1800 > 1650






    1800




    >








    1650








  • 1750

    ,

    1650

    ,

    1700

    ,

    1850

    ,

    1900

    ,

    1800

    1750, 1650, 1700, 1850, 1900, 1800






    1750


    ,




    1650


    ,




    1700


    ,




    1850


    ,




    1900


    ,




    1800





    ,因为



    1750

    <

    1800

    1750 < 1800






    1750




    <








    1800





因此,最终排出的队形是



1750

,

1650

,

1700

,

1850

,

1900

,

1800

1750, 1650, 1700, 1850, 1900, 1800






1750


,




1650


,




1700


,




1850


,




1900


,




1800





小 A 心中有一个理想队形,他想知道多少种初始队形可以获得理想的队形。

请求出答案对



19650827

19650827






19650827





取模的值。


输入格式

第一行一个整数



n

n






n







第二行



n

n






n





个整数,表示小 A 心中的理想队形。


输出格式

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



m

o

d

19650827

\bmod 19650827










mod






19650827





的值。


样例输入 #1

4
1701 1702 1703 1704


样例输出 #1

8


提示

对于



30

%

30\%






30%





的数据,



n

100

n \le 100






n













100







对于



100

%

100\%






100%





的数据,



n

1000

n \le 1000






n













1000









1000

h

i

2000

1000 \le h_i \le 2000






1000














h










i





























2000






思路

  • 状态:



    f

    [

    i

    ]

    [

    j

    ]

    [

    0

    /

    1

    ]

    f[i][j][0/1]






    f


    [


    i


    ]


    [


    j


    ]


    [


    0/1


    ]





    为目标队形中



    [

    i

    ,

    j

    ]

    [i,j]






    [


    i


    ,




    j


    ]





    区间的方案数,且最后一个人在最左/右;状态转移:




    • f

      [

      i

      ]

      [

      j

      ]

      [

      0

      ]

      f[i][j][0]






      f


      [


      i


      ]


      [


      j


      ]


      [


      0


      ]





      =



      f

      [

      i

      +

      1

      ]

      [

      j

      ]

      [

      0

      ]

      [

      h

      i

      <

      h

      i

      +

      1

      ]

      +

      f

      [

      i

      +

      1

      ]

      [

      j

      ]

      [

      1

      ]

      [

      h

      i

      <

      h

      j

      ]

      f[i+1][j][0]*[h_i<h_{i+1}]+f[i+1][j][1]*[h_i<h_j]






      f


      [


      i




      +








      1


      ]


      [


      j


      ]


      [


      0


      ]













      [



      h










      i




















      <









      h











      i


      +


      1



















      ]




      +








      f


      [


      i




      +








      1


      ]


      [


      j


      ]


      [


      1


      ]













      [



      h










      i




















      <









      h










      j


















      ]







    • f

      [

      i

      ]

      [

      j

      ]

      [

      1

      ]

      =

      f

      [

      i

      ]

      [

      j

      1

      ]

      [

      0

      ]

      [

      h

      j

      >

      h

      i

      ]

      +

      f

      [

      i

      +

      1

      ]

      [

      j

      ]

      [

      1

      ]

      [

      h

      j

      >

      h

      j

      1

      ]

      f[i][j][1]=f[i][j-1][0]*[h_j>h_i]+f[i+1][j][1]*[h_j>h_{j-1}]






      f


      [


      i


      ]


      [


      j


      ]


      [


      1


      ]




      =








      f


      [


      i


      ]


      [


      j













      1


      ]


      [


      0


      ]













      [



      h










      j




















      >









      h










      i


















      ]




      +








      f


      [


      i




      +








      1


      ]


      [


      j


      ]


      [


      1


      ]













      [



      h










      j




















      >









      h











      j





      1



















      ]





题解

#include <bits/stdc++.h>
using namespace std;
int f[2010][2010][2],a[2010];
int main(){
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=n;i++)f[i][i][0]=1;
	for(int len=1;len<=n;len++)
		for(int i=1,j=i+len;j<=n;i++,j++){
			if(a[i]<a[i+1])f[i][j][0]+=f[i+1][j][0];
			if(a[i]<a[j])f[i][j][0]+=f[i+1][j][1];
			if(a[j]>a[i])f[i][j][1]+=f[i][j-1][0];
			if(a[j]>a[j-1])f[i][j][1]+=f[i][j-1][1];
			f[i][j][0]%=19650827;
			f[i][j][1]%=19650827;
		}
	cout<<(f[1][n][0]+f[1][n][1])%19650827;
}



总结

当我们需要求解一个序列中某个子区间的最优解时,可以考虑使用区间dp算法。具体来说,我们需要思考以下几个问题:

  1. 状态设计:需要定义一个状态表示子区间的最优解。状态的设计通常与问题的性质密切相关,可以根据实际情况选择不同的状态表示方式。
  2. 状态转移方程:需要设计一个状态转移方程,根据已知状态推导出未知状态。状态转移方程通常是一个递推式,可以根据问题的性质和状态的定义来确定。
  3. 边界条件:需要确定最小的子问题的最优解,即边界条件。边界条件通常是一个或多个初始状态,可以根据实际情况选择不同的初始状态。
  4. 时间复杂度:需要考虑算法的时间复杂度,尽可能地优化算法实现,避免不必要的计算量和空间占用。


    往往时间复杂度都在



    O

    (

    n

    2

    )

    O(n^2)






    O


    (



    n










    2









    )





    ,所以数据量如果过大则不宜适用这个算法

设计区间dp算法的关键在于状态设计和状态转移方程的确定。在实际操作中,可以先尝试暴力枚举所有子区间,然后根据已知的子区间最优解推导出更大的子区间最优解,最终得到整个序列的最优解。如果时间复杂度过高,则需要考虑优化算法实现,例如使用记忆化搜索等方法来避免重复计算。



版权声明:本文为qq_61786525原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。