Loading

线性Dp

总述:

本博客就是线性dp模板+看到模板的总结

壹.LIS模板

description

设有由n个不相同的整数组成的数列,记为:b(1)、b(2)、……、b(n)且b(i)<>b(j) (i<>j),若存在i1<i2<i3< < ie 且有b(i1)<b(i2)< <b(ie)则称为长度为e的不下降序列。程序要求,当原数列出之后,求出最长的上升序列。

例如13,7,9,16,38,24,37,18,44,19,21,22,63,15。例中13,16,18,19,21,22,63就是一个长度为7的不下降序列,同时也有7 ,9,16,18,19,21,22,63长度为8的不下降序列。

本类模板一共有四个,分别是最长不下降,最长上升,最长不上升,最长下降,光看字面意思也很好理解了

初步分析

本题题目要求最长,也就是之前写的max/min类,根据题目我们需要确定状态

首先,必不可少的是这个数的大小

其次,既然要求最长,我们还要搞一个有关长度的状态,也就是对于一个下标i,表示从1到i不下降的序列有多长

最后,他要求出这个序列,属于记录路径的dp,一般对于记录路径的dp,我们采用索引父节点的方法进行记录(背包Dp中也有具体题目)

进一步分析

我们可以通过一个结构体来实现分析所说(尽管我不知道为什么大多数人喜欢开二维数组,我觉得结构体更加清晰,不会让自己记错


struct Point{

  int data ;

  int len ;

  int next ;

}

分析状态转移

有了状态之后,如何进行转移呢?

我们考虑最后一步或者任意一步

在这个序列中,任意一个数从1到i的长度可以分为一下两种情况

1.如果这个数(假设下标为i)大于等于它之前的数(假设下标为j),那么从i的len就是j的len再加上一

2.如果i小于j,那么1到i的len就是1到j的len

状态转移实现


if( i > j && F [i].data >= F [j].data )

F [i].len = max( F [i].len , F [j].len + 1 ) ;

code

view code

#include<bits/stdc++.h>

#define int long long

using namespace std ;



const int N = 1010 ;



struct E{

  int data ;

  int len ;

  int next ;

}a [N] ;

int n ;



void sc(){

  scanf( "%lld" , &n ) ;

  for( int i = 1 ; i <= n ; i ++ )

  scanf( "%lld" , & a [i].data ) ;

}



void ups(){

 /* int maxn = 0 ;

  for( int i = 2 ; i <= n ; i ++ ){

​    for( int j = i - 1 ; j >= 1 ; j -- ){

​      if( a [i].data > a [j].data )

​      a [i].len = max( a [i].len , a [j].len + 1 ) ;

​    }

​    maxn = max( maxn , a [i].len ) ;

  }*/

//  如果不需要求出这个序列,那么这样简洁的代码就足够了

  int ans = 0 ;

  for( int i = 2 ; i <= n ; i ++ ){

​    int maxn = 0 , maxi = 0 ; 

​    for( int j = i - 1 ; j >= 1 ; j -- ){

​      if( a [i].data > a [j].data && a [j].len > maxn )

​      maxn = a [j].len , maxi = j ;

​    }

​    a [i].len = a [j].len + 1 ;

​    a [i].next = j ; 

  }

  int maxn = 0 , maxi = 0 ;

  for( int i = 1 ; i <= n ; i ++ ){

​    if( a [i].len > maxn ) 

​    maxn = a [i].len , maxi = i ;

  }

  printf( "MAXN = %lld" , maxn ) ;

  while( maxi ){

​    printf( "%lld" , a [maxi].len ) ;

​    maxi = a [maxi].next ;

  }

}



void noups(){

 /* int maxn = 0 ;

  for( int i = 2 ; i <= n ; i ++ ){

​    for( int j = i - 1 ; j >= 1 ; j -- ){

​      if( a [i].data >= a [j].data )

​      a [i].len = max( a [i].len , a [j].len + 1 ) ;

​    }

​    maxn = max( maxn , a [i].len ) ;

  }

  如果不需要求出这个序列,那么这样简洁的代码就足够了*/

  int ans = 0 ;

  for( int i = 2 ; i <= n ; i ++ ){

​    int maxn = 0 , maxi = 0 ; 

​    for( int j = i - 1 ; j >= 1 ; j -- ){

​      if( a [i].data >= a [j].data && a [j].len > maxn ) // 唯一的区别就是这个等号

​      maxn = a [j].len , maxi = j ;

​    }

​    a [i].len = a [j].len + 1 ;

​    a [i].next = j ; 

  }

  int maxn = 0 , maxi = 0 ;

  for( int i = 1 ; i <= n ; i ++ ){

​    if( a [i].len > maxn ) 

​    maxn = a [i].len , maxi = i ;

  }

  printf( "MAXN = %lld" , maxn ) ;

  while( maxi ){

​    printf( "%lld" , a [maxi].len ) ;

​    maxi = a [maxi].next ;

  }

}



void downs(){

  /*int maxn = 0 ;

  for( int i = 2 ; i <= n ; i ++ ){

​    for( int j = i - 1 ; j >= 1 ; j -- ){

​      if( a [i].data < a [j].data )

​      a [i].len = max( a [i].len , a [j].len + 1 ) ;

​   }

​    maxn = max( maxn , a [i].len ) ;

  }*/

  //如果不需要求出这个序列,那么这样简洁的代码就足够了*

  int ans = 0 ;

  for( int i = 2 ; i <= n ; i ++ ){

​    int maxn = 0 , maxi = 0 ; 

​    for( int j = i - 1 ; j >= 1 ; j -- ){

​      if( a [i].data < a [j].data && a [j].len > maxn ) //唯一的区别,大小于号不一样

​      maxn = a [j].len , maxi = j ;

​    }

​    a [i].len = a [j].len + 1 ;

​    a [i].next = j ; 

  }

  int maxn = 0 , maxi = 0 ;

  for( int i = 1 ; i <= n ; i ++ ){

​    if( a [i].len > maxn ) 

​    maxn = a [i].len , maxi = i ;

  }

  printf( "MAXN = %lld" , maxn ) ;

  while( maxi ){

​    printf( "%lld" , a [maxi].len ) ;

​    maxi = a [maxi].next ;

  }

}



void nodowns(){

  /*int maxn = 0 ;

  for( int i = 2 ; i <= n ; i ++ ){

​    for( int j = i - 1 ; j >= 1 ; j -- ){

​      if( a [i].data <= a [j].data )

​      a [i].len = max( a [i].len , a [j].len + 1 ) ;

​    }

​    maxn = max( maxn , a [i].len ) ;

  }*/

  //如果不需要求出这个序列,那么这样简洁的代码就足够了

  int ans = 0 ;

  for( int i = 2 ; i <= n ; i ++ ){

​    int maxn = 0 , maxi = 0 ; 

​    for( int j = i - 1 ; j >= 1 ; j -- ){

​      if( a [i].data <= a [j].data && a [j].len > maxn ) //这里是小于等于

​      maxn = a [j].len , maxi = j ;

​    }

​    a [i].len = a [j].len + 1 ;

​    a [i].next = j ; 

  }

  int maxn = 0 , maxi = 0 ;

  for( int i = 1 ; i <= n ; i ++ ){

​    if( a [i].len > maxn ) 

​    maxn = a [i].len , maxi = i ;

  }

  printf( "MAXN = %lld" , maxn ) ;

  while( maxi ){

​    printf( "%lld" , a [maxi].len ) ;

​    maxi = a [maxi].next ;

  }

}





signed main(){

  sc() ;

  ups() ;

  noups() ;

  downs() ;

  nodowns() ;

  return 0 ;

}

example

view example

结果

进一步思考

如果数据范围过大,\(N^2\)的算法肯定还是撑不住的,其实,lis算法还可以优化为\(\Theta(nlogn)\)的算法,是通过

( 遍历(n)* 二分查找(logn) ) = nlogn

如果此处不是二分查找,那么他还是一个\(N^2\)的算法

来实现的。

先拿最长上升为例

具体思路是,开一个数组( f []),让这个数组里面存的就是这个LIS

1.先把第一个数扔到f中,然后从原序列( a[])的第二个数字开始遍历

2.如果 a中的数字比f的最上端还要大,那就让这个a中的数字添加到f上端(保持f的严格单调递增),我们的答案也就多了一位(多加了一个数字

3.如果a中的数字比f的顶端小,我们就在数组中给这个新数字找一个合适的位置放下

4.什么位置适合这个新数字呢?我们只需要在f中找到最后一个比他大的数字,然后将其替换掉就可以。(注意,本处最后一个比他大的数字很有操作,我们在这个位置替换,既可以保证原序列的单调性。也创造了更多可能(序列中的数字越小,后面加到序列中的可能性就越大))


下面让我们对这个算法进行模拟,加深对它的理解

设这串序列为( a[] ){2,1,6,5,8,0,1,5,10}

1.因为没有数字,f[]={2}

2.因为1比2小,1无法接在任何数字后面,所以替代2,f[]={1}

3.因为6比任何一个数都要大,所以接在最前面,f[]={1,6}

4.5可以接在1后面并且比6要小,以此代替6,f[]={1,5}

5.因为8比任何数要大,接在末尾,f[]={1,5,8}

6.0比任何数都要小,所以f[]={0,5,8}

7.1可以接在0后面,所以f[]={0,1,8}

8.5可以接在8后面,所以f[]={0,1,5}

9.10可以接在5后面,所以f[]={0,1,5,10}

因为f[4]不为0,说明可以构成长度为4的最长上升子序列,故答案为4


我们可以通过使用STL中的函数来达到这一目的

首先,介绍一下这两个STL函数(离散化中也会用到)

lower_bound( )和upper_bound( )是利用二分查找的方法在一个有序的数组中进行查找的。(因为我们在f数组中查找,f数组我们是用过增加和替换来保持它的(严格)单调性的,故不用sort)

当数组是从小到大时,

lower_bound( begin,end,num ) :表示从数组的begin位置到end-1位置二分查找第一个大于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,找到数字在数组中的下标。

upper_bound( begin,end,num ):表示从数组的begin位置到end-1位置二分查找第一个大于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,找到数字在数组中的下标。

当数组从大到小时

理论上需要写一个cmp函数来重载,这里,发扬懒人风格,我们还是用stl里面给出来的重载 greater< int > (),那么就会有

lower_bound( begin,end,num,greater< int >() ):从数组的begin位置到end-1位置二分查找第一个小于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。

upper_bound( begin,end,num,greater< int >() ):从数组的begin位置到end-1位置二分查找第一个小于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。

他们四个的区别就是大小等于不同

  • 当寻找严格单调的序列时,我们找大于等于/小于等于的进行替换,因为

既然是严格递增,那么队列里面就不容许相等的元素,固见到相等的写要给他踢掉

  • 当寻找单调序列时候我们只需要找大于/小于的

  • 记忆:lower带等于号,重载从大到小

那么给出求四种LIS的代码

view code


#include<bits/stdc++.h>

#define int long long 

using namespace std ;



const int N = 1e5 + 10 ;



inline int read(){

  int w = 0 ; char ch = getchar() ;

  while( ch < '0' || ch > '9' ) ch = getchar() ;

  while( ch >= '0' && ch <= '9' ){

​    w = ( w << 1 ) + ( w << 3 ) + ( ch - '0' ) ;

​    ch = getchar() ;

  } return w ;

}



int n ;

int a [N] ;

int f [N] ;

int cnt = 0 ;



void sc(){

  n = read() ;

  for( int i = 1 ; i <= n ; i ++ )

  a [i] = read() ;

}



void ups(){

  memset( f , 0 , sizeof( f ) ) ;

  cnt = 0 ;

  f [++ cnt] = a [1] ;

  for( int i = 2 ; i <= n ; i ++ ){

​    if( a [i] > f [cnt] ) f [++ cnt] = a [i] ;

​    else{

​      int tmp = lower_bound( f + 1 , f + cnt + 1 , a [i] ) - f ;

​      f [tmp] = a [i] ;

​    } 

  }

  printf( "\nMAXN%lld\n" , cnt ) ;

  for( int i = 1 ; i <= cnt ; i ++ )

  printf( "%lld " , f [i] ) ;

}



void noups(){

  memset( f , 0 , sizeof( f ) ) ;

  cnt = 0 ;

  f [++ cnt] = a [1] ;

  for( int i = 2 ; i <= n ; i ++ ){

​    if( a [i] >= f [cnt] ) f [++ cnt] = a [i] ;

​    else{

​      int tmp = upper_bound( f + 1 , f + cnt + 1 , a [i] ) - f ;

​      f [tmp] = a [i] ;

​    } 

  }

  printf( "\nMAXN%lld\n" , cnt ) ;

  for( int i = 1 ; i <= cnt ; i ++ )

  printf( "%lld " , f [i] ) ;

}



void downs(){

  memset( f , 0 , sizeof( f ) ) ;

  cnt = 0 ;

  f [++ cnt] = a [1] ;

  for( int i = 2 ; i <= n ; i ++ ){

​    if( a [i] < f [cnt] ) f [++ cnt] = a [i] ;

​    else{

​      int tmp = lower_bound( f + 1 , f + cnt + 1 , a [i] , greater<int> () ) - f ;

​      f [tmp] = a [i] ;

​    } 

  }

  printf( "\nMAXN%lld\n" , cnt ) ;

  for( int i = 1 ; i <= cnt ; i ++ )

  printf( "%lld " , f [i] ) ;

}



void nodowns(){

  memset( f , 0 , sizeof( f ) ) ;

  cnt = 0 ;

  f [++ cnt] = a [1] ;

  for( int i = 2 ; i <= n ; i ++ ){

​    if( a [i] <= f [cnt] ) f [++ cnt] = a [i] ;

​    else{

​      int tmp = upper_bound( f + 1 , f + cnt + 1 , a [i] ,greater<int> ()) - f ;

​      f [tmp] = a [i] ;

​    } 

  }

  printf( "\nMAXN%lld\n" , cnt ) ;

  for( int i = 1 ; i <= cnt ; i ++ )

  printf( "%lld " , f [i] ) ;

}



signed main(){

  //四个函数的不同就是大小于号的地方和lower/upper和运算符的重载

  sc() ;

  ups() ;

  noups() ;

  downs() ;

  nodowns() ;

  return 0 ;

}



可以发现,不仅代码效率更高了,还更加简短了,真不错

也可以记录路径,只需要开一个记录数组就可以了

#include <cstring>
#include <cstdio>
#include <algorithm>			
#define R register int
#define printf tz1 = printf

using namespace std ;
const int N = 1e5 + 10 ;
typedef long long L ;
typedef double D ;

inline int read(){
	int w = 0 ; bool fg = 0 ; char ch = getchar() ;
	while( ch > '9' || ch < '0' ) fg |= ( ch == '-' ) , ch = getchar() ;
	while( ch >= '0' && ch <= '9' ) w = ( w << 1 ) + ( w << 3 ) + ( ch - '0' ) , ch = getchar() ;
	return fg ? -w : w ; 
}

int tz1 ;
int n , top , cnt , id [N] , p [N] , s [N] , c [N] ;


void sc(){
	while( ~scanf( "%d" , & c [++ n] ) ) ;
}

void work(){
	s [0] = -0x7fffffff ;
	for( R i = 1 ; i <= n ; i ++ ){
		if( s [top] <= c [i] ) s [++ top] = c [i] , id [top] = i , p [i] = id [top - 1] ;
		
		int l = 1 , r = top ;
		while( l < r ){ // 找到第一个大于等于c[i]的数
			int mid = ( l + r ) >> 1 ;
			if( s [mid] <= c [i] ) l = mid + 1 ;
			else r = mid ;
		} s [l] = c [i] , id [l] = i , p [i] = id [l - 1] ;
	}
	
	printf( "%d\n" , top ) ;
	int now = id [top] ;
	while( now ) printf( "%d\n" , c [now] ) , now = p [now] ;
}

signed main(){
	sc() ;
	work() ;
	return 0 ; 
}

LIS板子的包装

一.合唱乐队

question

  view question

\(N\)位同学站成一排,音乐老师要请其中的(\(N-K\))位同学出列,使得剩下的\(K\)位同学排成合唱队形。

合唱队形是指这样的一种队形:设K位同学从左到右依次编号为\(1,2,…,K\),他们的身高分别为\(T_1,T_2,…,T_K\),则他们的身高满足T_1<...< T_i >T_{i+1}>…>T_K\((1 \le i \le K)\)

你的任务是,已知所有\(N\)位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。

输入输出格式

输入格式


共二行。

第一行是一个整数\(N(2 \le N \le 100)\),表示同学的总数。

第二行有\(n\)个整数,用空格分隔,第\(i\)个整数\(T_i(130 \le T_i \le 230)\)是第\(i\)位同学的身高(厘米)。

输出格式


一个整数,最少需要几位同学出列。

输入输出样例

输入样例 #1

8

186 186 150 200 160 130 197 220

输出样例 #1

4

说明

对于50%的数据,保证有\(n \le 20\)

对于全部的数据,保证有\(n \le 100\)

thoughts

view thoughts

本题包装纸很薄,稍加思考就可以得出,我们要找一个位置,使得从这个位置向外,最长下降和最长上升的和最小,我们可以求出正向的增和逆向的降(正向降逆向增也可以),最后把len加起来就可以了,注意一下,拿n减去的时候需要加上1(不加连样例都过不了),因为自己减了两遍

code

view code

#include<bits/stdc++.h>

#define int long long

#define N 1010

using namespace std ;



struct point{

  int data ;

  int uplen ;

  int downlen ;

} a [N] ;

int n ;



void sc(){

  scanf( "%lld" , &n ) ;

  for( int i = 1 ; i <= n ; i ++ )

  scanf( "%lld" , & a [i].data ) , a [i].uplen = a [i].downlen = 1 ; 

}



void work() {

  for( int i = 2 ; i <= n ; i ++ ){

​    int maxn = 0 ;

​    for( int j = i - 1 ; j >= 1 ; j -- ){

​      if( a [j].data < a [i].data ) 

​        maxn = max( maxn , a [j].uplen ) ;

​    }

​    a [i].uplen = maxn + 1 ;

  }

  for( int i = n - 1 ; i >= 1 ; i -- ){

​    int maxn = 0 ;

​    for( int j = i + 1 ; j <= n ; j ++ ){

​      if( a [i].data > a [j].data )

​        maxn = max( maxn , a [j].downlen ) ;

​    }

​    a [i].downlen = maxn + 1 ;

  }

}



void pr(){

  int maxn = 0 , maxi ;

  for( int i = 1 ; i <= n ; i ++ )

​    maxn = max( maxn , a [i].uplen + a [i].downlen ) ;

  printf( "%lld\n" , n - maxn + 1 ) ;

}





signed main(){

  sc() ;

  work() ;

  pr() ;

  return 0 ;

}

二.导弹拦截

question

view question

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

输入导弹依次飞来的高度(雷达给出的高度数据是\(\le 50000\)的正整数),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

输入输出格式

输入格式

\(1\)行,若干个整数(个数$ \le 100000$)

\(2\)行,每行一个整数,第一个数字表示这套系统最多能拦截多少导弹,第二个数字表示如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

输入输出样例

输入样例 #1

389 207 155 300 299 170 158 65

输出样例 #1

6

2

thoughts

view thoughts

本题不仅要求最长的链的长度,还要求链的最小条数,这里引入一个定理,即可解决这个问题 ,这个定理的证明由于我也不会,我就不从百度上搬运了。

Dilworth定理内容

定理:令(X,≤)是一个有限偏序集,并令m是反链的最大的大小。则X可以被划分成m个但不能再少的链

通俗解释:把一个数列划分成最少的最长不升子序列的数目就等于这个数列的最长上升子序列的长度(LIS)”

其逆定理也成立

逆定理:令(X,≤)是一个有限偏序集,并令r是其最大链的大小。则X可以被划分成r个但不能再少的反链

有了这个定理,我们正着算一遍,反着算一遍,就可以直接给答案了

code

view code

#include<bits/stdc++.h>

#define int long long

#define N 100010

#define find shit

using namespace std ;



int a [N] ;



int n ;

int pass ;

int sum ;

bool fg ;  

int find [N] ;

struct cmp{

  bool operator()( int a , int b ){

​    return a > b ; 

  }

} ;



void sc(){

  while( scanf( "%lld" , & a [++ n] ) != EOF ) ; 

  n -- ;

}



void work() {

  int tot = 0 ;

  find [++ tot] = a [1] ;

  for( int i = 2 ; i <= n ; i ++ ){

​    if( a [i] <= find [tot] ) find [++ tot] = a [i] ;

​    else{

​      int tmp = upper_bound( find + 1 , find + 1 + tot , a [i] , cmp() ) - find ;

​      find [tmp] = a [i] ;  

​    }

  }

  printf( "%lld\n" , tot ) ;

}



void other(){

  int tot = 0 ;

  memset( find , 0 , sizeof( find ) ) ;

  find [++ tot] = a [1];

  for( int i = 2 ; i <= n ; i ++ ){

​    if( a [i] > find [tot] ) find [++ tot] = a [i] ; 

​    else{

​      int tmp = lower_bound( find + 1 , find + 1 + tot , a [i] ) - find ; 

​      find [tmp] = a [i] ;

​    }

  }

  printf( "%lld" , tot ) ;

}





signed main(){

  //需要注意的是大于小于等于三个号并起来要不重不漏,本题导弹大于等于,故反链的时候要严格小于*

  sc() ;

  work() ;

  other() ;

  return 0 ;

}

三.友好城市

question

view question

题目描述

有一条横贯东西的大河,河有笔直的南北两岸,岸上各有位置各不相同的N个城市。北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城市不相同。每对友好城市都向政府申请在河上开辟一条直线航道连接两个城市,但是由于河上雾太大,政府决定避免任意两条航道交叉,以避免事故。编程帮助政府做出一些批准和拒绝申请的决定,使得在保证任意两条航道不相交的情况下,被批准的申请尽量多。

输入输出格式

输入格式


第1行,一个整数N,表示城市数。

第2行到第n+1行,每行两个整数,中间用一个空格隔开,分别表示南岸和北岸的一对友好城市的坐标。

输出格式


仅一行,输出一个整数,表示政府所能批准的最多申请数。

输入输出样例

输入样例 #1

7

22 4

2 6

10 3

15 12

9 8

17 17

4 2

输出样例 #1

4

说明

50% 1<=N<=5000,0<=xi<=10000

100% 1<=N<=2e5,0<=xi<=1e6

thoughts

view thoughts

我们只需要将所有港口的一侧从小到大排序,那么另一侧的最长上升子序列就是答案.

这道题乍一看没思路,我们只需要将条件数字化即可,数字化之后,我们需要求的就是i1 < i2 且 j1 < j2 或 i1 > i2 且 j1 > j2 ,但是,同时满足两个单调上升的子序列咋写啊?不难想到,将一侧排序后,按照新的顺序给另一侧求单调上升序列即可

code

view code

#include<iostream>

#include<algorithm>

#define MaxN 500005

using namespace std;

int Num, Dp[MaxN], Ans;

struct Data

{

  int South, North;

} Cities[MaxN];

bool Cmp(Data X, Data Y)

{

  return ( X.South < Y.South );

}

int main()

{

  cin >> Num;

  for(int i = 0; i < Num; i++)

  {

​    cin >> Cities[i].South >> Cities[i].North;

  }

  sort(Cities, Cities + Num, Cmp);  *//排序*

  for(int i = 0; i < Num; i++)

  {

​    if(i == 0 || Cities[i].North > Dp[Ans - 1])

​    {

​      Dp[Ans] = Cities[i].North;

​      Ans++;

​    }

​    else

​    {

​      int Place = lower_bound(Dp, Dp + Ans, Cities[i].North) - Dp;  //二分查找

​      Dp[Place] = Cities[i].North;

​    }

  }

  cout << Ans;

  return 0;

}

四.飞翔

question

view question

鹰最骄傲的就是翱翔,但是鹰们互相都很嫉妒别的鹰比自己飞的快,更嫉妒其他的鹰比自己飞行的有技巧。于是,他们决定举办一场比赛,比赛的地方将在一个迷宫之中。

描述

这些鹰的起始点被设在一个N*M矩阵的左下角map[1,1]的左下角。终点被设定在矩阵的右上角map[N,M]的右上角,有些map[i,j]是可以从中间穿越的。每一个方格的边长都是100米。如图所示: 


没有障碍,也没有死路。这样设计主要是为了高速飞行的鹰们不要发现死路来不及调整而发生意外。潘帕斯雄鹰冒着减RP的危险从比赛承办方戒备森严的基地中偷来了施工的地图。但是问题也随之而来,他必须在比赛开始之前把地图的每一条路都搞清楚,从中找到一条到达终点最近的路。(哈哈,笨鸟不先飞也要拿冠军)但是此鹰是前无古鹰,后无来鹰的吃菜长大的鹰--菜鸟。他自己没有办法得出最短的路径,于是紧急之下找到了学OI的你,希望找到你的帮助。

格式

输入格式

首行为n,m(0<n,m<=1000000),第2行为k(0<k<=1000)表示有多少个特殊的边。以下k行为两个数,i,j表示map[i,j]是可以直接穿越的。

输出格式

仅一行,1,1-->n,m的最短路径的长度,四舍五入保留到整数即可

样例1

样例输入1

3 2

3

1 1

3 2

1 2

样例输出1

383

thoughts

view thoughts

从左下角走到右上角,可以很轻松的证明走的斜边越多,路程越小,故本题转化为求走最多的斜边。

当一条斜边的横纵坐标都比另一条小的时候,我们可以同时走这两条边,条件数字化后,可知当x1>x2且y1>y2可以走。

但是,如果走了一些不好的边,比如走了样例图片中没有标黑的那条特殊路段,就只能走一条特殊边,我们称这样的作用为后效性,为了排除后效性,常用的方法有排序(当然有的题根本就无法排除后效性,那他就不能用dp来解),如果我们按x为第一标准,y为第二标准从小到大进行排序,就排除后效性了(当然按y为第一保准也可以),因排完序后我们可以保证走完这条边后有足够的边可以走【可以考虑反证法证明。如果有{ 一条x大于另一条边,y相等 OR y大于另一条边,x相等 OR x和y都大于另一条边 }这三种边中的任意一个 ,那么我们可以证明,大的那条边能走的边小的都可以走,而如果选择大的那则不一定,所以为了选尽量多的边(排除后效性),我们将他排序 】。这也就是二维LIS的解法,(类似题还有信封嵌套LEETCODE.354 LuoGu P2285 )

code

view code

#include<bits/stdc++.h>

#define int long long

#define N 1010

using namespace std ;



struct point{

  int x ;

  int y ;

  int len ;

} a [N] ;



bool com( point a , point b ){

  if( a.y == b.y ) return a.x < b.x ;

  return a.y < b.y ;

}





inline int read(){

  int w = 0 ;

  char ch = getchar() ;

  while( ch < '0' || ch > '9' ) ch = getchar() ;

  while( ch >= '0' && ch <= '9' ){

​    w = ( w << 1 ) + ( w << 3 ) + ( ch - '0' ) ;

​    ch = getchar() ;

  }return w ; 

}



int n , m , k ;



void sc(){

  n = read() , m = read() , k = read() ;

  for( int i = 1 ; i <= k ; i ++ ) a [i].x = read() , a [i].y = read() , a [i].len = 1 ;

  sort( a + 1 , a + 1 + k , com ) ;  

}



void work(){

  int maxn = 0 ;

  for( int i = 2 ; i <= k ; i ++ ){

​    for( int j = 1 ; j < i ; j ++ ){

​      if( a [j].x < a [i].x && a [j].y < a [i].y )

​      a [i].len = max( a [j].len + 1 , a [i].len ) ;

​    } 

​    maxn = max( maxn , a [i].len ) ;

  }

  double X ;

  X = ( m + n + ( ( sqrt( 2.0 ) - 2 ) * maxn ) ) * 100 ;

  int ans = X + 0.5 ;

  printf( "%lld" , ans ) ;

}





signed main(){

  sc() ;

  work() ;

  return 0 ;

} 

贰.LCS模板

Description

给定两个序列,求他们最长的公共子序列的长度

给定两个序列,求他们最长的公共子串的长度

子序列可以不连续,子串可以不连续

对于子序列,先确定状态,就是对于i,j两个位置从1到i和从1到j的公共长度有多少(dp中常用这种类似前缀方法,因为需要状态转移,我们可以由状态转移的方法反推状态)

分析任意一步,如i==j,那么F[i][j] = F[i-1][j-1] + 1

如果i!=j 那么,F[i][j] = max( F [i-1][j] , F [i][j-1] )

如果i=0 || j=0 ,那么F[i][

对于子串,没有那么多花里胡哨的状态转移,因为必须连续

所以我们得到,如果A[i] == B [j] 那么f[i][j] = f[i-1][j-1] + 1

如果不等的话,就是0,我们可搞一个ans变量来记录最大

code

view code

#include<bits/stdc++.h>

#define int long long

#define N 1010

using namespace std ;



char a [N] , b [N] ; 

int t [N][N] ;





void sc(){ scanf( "%s%s" , a + 1 , b + 1 ) ; }



void workchuan(){

  int ans = 0 ;

  int len1 = strlen( a + 1 ) , len2 = strlen( b + 1 ) ;

  for( int i = 1 ; i <= len1 ; i ++ ){

​    for( int j = 1 ; j <= len2 ; j ++ ){

​      if( a [i] == b [j] )

​      t [i][j] = t [i-1][j-1] + 1 ;

​      ans = max( ans , t [i][j] ) ;

​    }

  }

  printf( "%lld" , ans ) ;

}



void workxulie(){

  for (int i = 1; i <= A.size(); ++i)

  {

​    for (int j = 1; j <= B.size(); ++j)

​    {

​      if (A[i-1] == B[j-1])

​      {

​        len[i][j] = len[i-1][j-1] + 1;

​      }else if (len[i-1][j] >= len[i][j-1])

​      {

​        len[i][j] = len[i-1][j];

​      }else{

​        len[i][j] = len[i][j-1];

​      }

​    }

//代码是白嫖的*

}





signed main(){

  sc() ;

  workchuan() ;

  workxulie() ;

  return 0 ;

}



子串同理,就是方程变成了$$f [i][j] = f [i - 1][j - 1] + 1(当且仅当s1[i] == s2 [j])$$

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

这样是\(n^2\)的算法,可以优化成\(nlogn\),具体方法参考这篇博客,是一个投射的思想,通过下标上升保证a序列上升,LIS保证b序列上升,最后再用二分栈优化一下

链接: https://www.zybuluo.com/ysner/note/1332445
代码

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <ctime>
#define R register int

using namespace std ;
typedef long long L ;
typedef long double D ;
const int N = 1e5 + 10 ;

inline int read(){
	int w = 0 ; bool fg = 0 ; char ch = getchar() ;
	while( ch > '9' || ch < '0' ) fg |= ( ch == '-' ) , ch = getchar() ;
	while( ch >= '0' && ch <= '9' ) w = ( w << 1 ) + ( w << 3 ) + ( ch - '0' ) , ch = getchar() ;
	return fg ? -w : w ; 
}

int s1 [N] , s2 [N] ;
int n , f [N] , m1 [N] , m2 [N] ;
int lc [N][6] ;
int op [N * 5] , cnt ;
int s [N * 5] ;

void sc(){
	n = read() ; n *= 5 ;
	for( R i = 1 ; i <= n ; i ++ ) s1 [i] = read() ;
	for( R i = 1 ; i <= n ; i ++ ) s2 [i] = read() , lc [s2 [i]][++ lc [s2 [i]][0]] = i ;
	for( R i = 1 ; i <= n ; i ++ )
	for( R j = 5 ; j >= 1 ; j -- )
	op [++ cnt] = lc [s1 [i]][j] ;	
//	for( R i = 1 ; i <= cnt ; i ++ ) printf( "%d " , op [i] ) ;
	//printf( "\n" ) ;
}

void work(){
	int top = 0 ;
	s [++ top] = op [1] ;
	for( R i = 2 ; i <= n * 5 ; i ++ ){
		if( op [i] > s [top] ) s [++ top] = op [i] ;
		else{
			int l = 1 , r = top ;
			while( l < r ){
				int mid = ( l + r ) >> 1 ; 
				if( op [i] > s [mid] ) l = mid + 1 ; 
				else r = mid ;
			} 
			s [l] = op [i] ;
		}
	}
	printf( "%d\n" , top ) ;
}

signed main(){
	sc() ;
	work() ;
	return 0 ;
}

LCS例题

回文词

题目描述

回文词是一种对称的字符串——也就是说,一个回文词,从左到右读和从右到 左读得到的结果是一样的。任意给定一个字符串,通过插入若干字符,都可以变成一个回文 词。你的任务是写一个程序,求出将给定字符串变成回文词所需插入的最少字符数。 比如字符串“Ab3bd”,在插入两个字符后可以变成一个回文词(“dAb3bAd” “Adb3bdA”)。然而,插入两个以下的字符无法使它变成一个回文词。

输入

文件的第行包含一个整数N,表示给定字符串的长度(3≤N≤5000)。

文件的第二行是一个长度为N的字符串。字符串仅由大写字母“A”到“Z”,小写字母“a” 到“z”和数字“0”到“9”构成。大写字母和小写字母将被认为是不同的。

输出

文件只有一行,包含一个整数,表示需要插入的最少字符数。

样例

样例输入

5
Ab3bd 

样例输出

2 

思路:仔细思考后不难发现,将原字符串reverse后的字符串和原来的字符串求LCS即可。另外还要注意一下:本题不可以开long long否则会爆空间。

code

view code
#include<bits/stdc++.h>
using namespace std ;

const int N = 5010 ;

char a [N] ;
char b [N] ;
int f [N][N] ;
int len ;

void sc(){
	int shit ;
	scanf( "%d" , &shit ) ;
	scanf( "%s" , a + 1 ) ;
	len = strlen( a + 1 ) ;
	memcpy( b , a , sizeof( a ) ) ;
	reverse( a + 1 , a + len + 1 ) ;
//	for( int i = 1 ; i <= len ; i ++ )
//	printf( "%c %c\n" , a [i] , b [i] ) ;
}

void work(){
	for( int i = 1 ; i <= len ; i ++ ){
		for( int j = 1 ; j <= len ; j ++ ){
			if( a [i] == b [j] ) 
			f [i][j] = f [i - 1][j - 1] + 1 ;
			else
			f [i][j] = max( f [i - 1][j] , f [i][j - 1] ) ;
//			printf( "%lld %lld %lld\n" , i , j , f [i][j] ) ;
		} 
	}
	printf( "%d" , len - f [len][len] ) ;
}

signed main(){
	sc() ;
	work() ;
	return 0 ;
}



叁.线性Dp杂题

1.奶牛过河

Farmer John以及他的N(1 <= N <= 2,500)头奶牛打算过一条河,但他们所有的渡河工具,仅仅是一个木筏。

由于奶牛不会划船,在整个渡河过程中,FJ必须始终在木筏上。在这个基础上,木筏上的奶牛数目每增加1,FJ把木筏划到对岸就得花更多的时间。

当FJ一个人坐在木筏上,他把木筏划到对岸需要M(1 <= M <= 1000)分钟。当木筏搭载的奶牛数目从i-1增加到i时,FJ得多花M_i(1 <= M_i <= 1000)分钟才能把木筏划过河(也就是说,船上有1头奶牛时,FJ得花M+M_1分钟渡河;船上有2头奶牛时,时间就变成M+M_1+M_2分钟。后面 的依此类推)。那么,FJ最少要花多少时间,才能把所有奶牛带到对岸呢?当然,这个时间得包括FJ一个人把木筏从对岸划回来接下一批的奶牛的时间。

输入格式

第1行: 2个用空格隔开的整数:N 和 M

第2..N+1行: 第i+1为1个整数:M_i

输出格式

第1行: 输出1个整数,为FJ把所有奶牛都载过河所需的最少时间

样例输入

5 10

3

4

6

100

1

样例输出

50

思路:

搞一个前缀和,再用一个数组从1枚举带i头牛过河的时间,通过将第i头牛的过河时间拆分,每次都拆成两份,因为之前的时间就是最优的,所以每次只用在拆出来的两份中进行判断

code

view code

#include<bits/stdc++.h>

#define int long long

#define N 2510

#define M 1010

using namespace std ;



*inline* int read(){

  int w = 0 ;

  char ch = getchar() ;

  while( ch < '0' || ch > '9' ) ch = getchar() ;

  while( ch >= '0' && ch <= '9' ) { 

​    w = ( w << 1 ) + ( w << 3 ) + ( ch - '0' ) ;

​    ch = getchar() ; 

  } return w ;

}



int n , m , t [N] ;

int F [N] , s [N] ;



void sc(){

  n = read() , m = read() ;

  s [0] += 2 * m ;

  for( int i = 1 ; i <= n ; i ++ )

  t [i] = read() , s [i] = s [i - 1] + t [i] ;

}



void work(){

  memset( F , 0x7f , sizeof( F ) ) ;

  for( int i = 1 ; i <= n ; i ++ ){

​    F [i] = s [i] ;

​    for( int j = 0 ; j < i ; j ++ )

​      F [i] = min( F [i] , F [j] + s [i - j] ) ;

  }

​    

  printf( "%lld" , F [n] - m ) ;

}





signed main(){

  sc() ;

  work() ; 

  return 0 ;

}



2.机器分配

题目描述

总公司拥有高效设备M台,准备分给下属的N个分公司。各分公司若获得这些设备,可以为国家提供一定的盈利。问:如何分配这M台设备才能使国家得到的盈利最大?求出最大盈利值。其中M≤15,N≤10。分配原则:每个公司有权获得任意数目的设备,但总台数不超过设备数M。

输入输出格式

输入格式

第一行有两个数,第一个数是分公司数N,第二个数是设备台数M。

接下来是一个N*M的矩阵,表明了第 I个公司分配 J台机器的盈利。

输出格式

第1行为最大盈利值

第2到第n为第i分公司分x台

P.S.要求答案的字典序最小

输入输出样例

输入1 :

3 3

30 40 50

20 30 50

20 25 30

输出1 :

70

1 1

2 1

3 1

思路:还是先推状态,状态不好想出来,就由方程倒逼状态,容易想出来的一个不正确的状态就是f(i,j)代表给第i个公司分j台的盈利,可是仔细思考,这个状态和其它的状态并关系,没有办法进行状态转移,所以还是运用前缀和的思想,f(i,j)代表前i个公司分j台机器盈利,我认为本题其实就是奶牛过河加了一维,由于前i个公司分配j台机器的盈利之前都算过了,为最优,所以每次i的分配只与i-1的有关,而不用将i-1再拆开搞(那就成dfs了,这就是dp记忆化搜索记下来的东西,因为再拆出来的还是已经记录的这个最优解,所以没有必要接着拆下去了),这样就不难给出方程f[i][j] = f [i-1][j-k] + w [i][k],通过枚举一遍j嵌套k得到当f等于i的时候的最优解,这样就可以给i+1用的时候记录下来

优化:由于本题f[i][] 只与f[i-1][]有关,可以考虑用滚动数组优化降维,这样就需要我们倒序进行遍历,那么当用f的时候里面存的自然就是i-1的了。

如果不需要记录路径,我们可以给出以下核心代码


for( int i = 1 ; i <= n ; i ++ ){

​	for( int j = m ; j >= 0 ; j -- ){

​		for( int k = 0 ; k <= j ; k ++ )

​			f [j] = f [j- k] + v [i] [j] ;

​	}

}

记录路径可以考虑用一个path数组来记录,不过i如果正序枚举,就会导致path数组记录错误,所以我们考虑将i也倒序枚举

code

view code
#include<bits/stdc++.h>
#define int long long
using namespace std ;

const int N = 20 ;

inline int read(){
    int w = 0 ; char ch = getchar() ;
    while( ch > '9' || ch < '0' ) ch = getchar() ;
    while( ch >= '0' && ch <= '9' ){
        w = ( w << 1 ) + ( w << 3 ) + ( ch - '0' ) ;
        ch = getchar() ;
    } return w ;
}

int n , m ;
int v [N][N] ;
int f [N] ;
int r [N][N] ;

void sc(){
    n = read() ; m = read() ;
    for( int i = 1 ; i <= n ; i ++ )
    for( int j = 1 ; j <= m ; j ++ )
        v [i][j] = read() ;
}

void work(){
    for( int i = n ; i >= 1 ; i -- ){
        for( int j = m ; j >= 0 ; j -- ){
            for( int k = 1 ; k <= j ; k ++ ){
                if( f [j - k] + v [i][k] > f [j])
                f [j] = f [j - k] + v [i][k] ,
                r [i][j] = k ;
            }
        }
    }
    printf( "%lld\n" , f [m] ) ;
    int i = 1 , j = m ;
    while( i <= n ){
        printf( "%lld %lld\n" , i , r [i][j] ) ;
        j -= r [i][j] ;
        i ++ ;
    }
    
}



signed main(){
    sc() ;
    work() ;
    return 0 ;
}

posted @ 2021-01-28 22:13  Soresen  阅读(194)  评论(0)    收藏  举报