La subsecuencia común más larga (LCS, por sus siglas en inglés) de la programación dinámica de C++ Análisis de casos clásicos_Pry en la consistencia de la recursividad y la programación dinámica

1. Introducción

En el caso de la programación dinámica que trata casos relacionados con personajes, la sumatoria 最长公共子序列y la sumatoria 最短编辑距离pueden considerarse casos clásicos en los clásicos.

Hay muchos algoritmos para explicar este tipo de problemas en Internet, aun así, todavía no puedo evitar tener la idea de escribir este artículo. Después de todo, la comprensión y la comprensión no se consideran un verdadero dominio. Solo cuando puedes ver el misterio y tienes tus propias percepciones únicas y diferentes percepciones puedes aprender el conocimiento profundamente.

¡está bien! Chismes menos, vamos al grano.

2. Subsecuencia común más larga (LCS)

2.1 Descripción del problema

La subsecuencia común más larga se refiere a 2la subsecuencia común más larga entre una o más cadenas de caracteres.

Como la cadena s1=kabcy s2=taijc, cuya subsecuencia común más larga es ac.

Sugerencias: la subsecuencia solo requiere que los caracteres permanezcan en el mismo orden que en la cadena original, no necesariamente continuos.

2.2 Pensamiento recursivo

Esta es una cuestión de buscar el mayor valor. Siempre que se busque el mayor valor, debe haber múltiples opciones . El principio es muy simple. Si no hay múltiples opciones, ¿es necesario enredar quién es el más grande y quién? ¿es el más pequeño?

Consejos: Hay manzanas, naranjas, plátanos frente a ti... solo puedes elegir uno, y solo entonces te enredarás. Si solo hay una manzana frente a ti, ¿aún estarás enredado?

Ante este problema, podemos adoptar la idea de descomponer el todo en partes, pasar del nivel macro al nivel micro y reducir la escala del problema mediante el pensamiento recursivo.

Si s1el puntero de posición se establece para la cadena iy s2el puntero de posición se establece para la cadena j, el problema se puede abstraer como la siguiente función. Semántica de la función: la subsecuencia común más larga de las cadenas icon y jcomo posiciones iniciales .s1,s2

int lcs(string s1,int i,string s2,int j);
//如果 s1、s2为全局变量,函数可以是
int lcs(int i,int j);  

41.png

  • Inicialmente, i=0suma significa encontrar la subsecuencia común más larga de la suma j=0completa . En este momento, la escala es la más grande y la respuesta no se puede obtener directamente. De esta manera, el problema se extiende a subproblemas más pequeños.s1s2

42.png

Como se mencionó anteriormente, debe haber múltiples opciones para encontrar el valor máximo.En el problema original k!=t, puede haber las siguientes 3opciones:

A. iNo se mueva, j+1. Es decir, continúe comparando la cadena iseñalada como posición inicial con la cadena de caracteres como posición inicial . puede considerarse como un subproblema.s1j+1s2

43.png

​ B. jNo se mueva, i+1. Es decir, continúe comparando la cadena i+1señalada como posición inicial con la cadena de caracteres como posición inicial . puede considerarse como otro subproblema.s1js2

[Falló la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo de enlace antirrobo, se recomienda guardar la imagen y cargarla directamente (img-Cr2f8B0w-1691975983175) (D:\Red Mud\My Course System\Data Structure y algoritmos\Serie de programación dinámica\images\44.png)]

C, iy jmuévase a la siguiente posición al mismo tiempo. Es decir, continúe comparando la cadena i+1señalada como posición inicial con la cadena de caracteres como posición inicial . También cuenta como una subpregunta.s1j+1s2

45.png

Es decir, cuando los caracteres de la pregunta original iy jque señalan la posición no son los mismos, hay 3opciones. En cuanto a cómo resolver los subproblemas, esto se debe al pensamiento recursivo.

Sugerencias: la mayor ventaja de la recursividad es que solo necesita determinar la función de la función básica y luego determinar el subproblema. Cómo resolver el problema interno del subproblema se puede ignorar desde una perspectiva macro. En cambio, puede continuar reduciendo el tamaño del problema paso a paso hasta que tenga una respuesta.

Luego, entre 3las opciones, devuelva la que tenga el mayor valor como resultado del problema actual.

int lcs(string s1,int i,string s2,int j){
    if(s1[i]!=s2[j]){
        //有 3 种选择
        int sel_1=lcs(s1,i,s2,j+1);
        int sel_2=lcs(s1,i+1,s2,j);
        int sel_3=lcs(s1,i+1,s2,j+1);
        return max(sel_1,sel_2,sel_3);
    } 
}
  • Como se muestra en la siguiente figura, cuando i和jlos valores de las posiciones señaladas son iguales, se debe encontrar un carácter común en el subproblema actual, y el resultado final es el resultado del subproblema posterior más 1, y la subsecuencia común más larga es el valor original más 1.

    Consejos: Al recoger conchas en la playa, se recoge una actualmente, y la concha que finalmente se puede recoger al regresar a casa debe ser la recogida actualmente más las conchas recogidas más tarde.

45.png

Al mismo tiempo, mueva iy j, a subproblemas más pequeños. Como se muestra abajo.

​ Llegados a este punto, se puede resumir que usar la recursividad para encontrar la subsecuencia común más larga es similar a jugar a Xiaoxiaole, si es la misma, será eliminada y entrará directamente al resto del contenido. No es lo mismo, habrá más opciones.

46.png

int lcs(string s1,int i,string s2,int j){
    if(s1[i]!=s2[j]){
        //有 3 种选择
        int sel_1=lcs(s1,i,s2,j+1);
        int sel_2=lcs(s1,i+1,s2,j);
        int sel_3=lcs(s1,i+1,s2,j+1);
         //三者之中选择最大返回值
    }else{
        //只有一个选择
        return lcs(s1,i+1,s2,j+1)+1;
    }
}
  • límites de recursión. En ese momentoi==s1.size() 或 j==s2.size() , significa que se ha escaneado el final de la subcadena. Como se muestra en la figura a continuación, no importa qué puntero llegue primero al final de la cadena, ya no hay ninguna subsecuencia común.

47.png

int lcs(string s1,int i,string s2,int j){
    if(i==s1.size() || j==s2.size())return 0;
    if(s1[i]!=s2[j]){
        //有 3 种选择
        int sel_1=lcs(s1,i,s2,j+1);
        int sel_2=lcs(s1,i+1,s2,j);
        int sel_3=lcs(s1,i+1,s2,j+1);
        //三者之中选择最大返回值
    }else{
        //只有一个选择
        return lcs(s1,i+1,s2,j+1)+1;
    }
}

Lo anterior se basa en el análisis recursivo del problema, el código completo es el siguiente:

#include <iostream>
using namespace std;
int lcs(string s1,int i,string s2,int j) {
	if(i==s1.size() || j==s2.size())return 0;
	if(s1[i]!=s2[j]) {
		//有 3 种选择
		int sel_1=lcs(s1,i,s2,j+1);
		int sel_2=lcs(s1,i+1,s2,j);
		int sel_3=lcs(s1,i+1,s2,j+1);
		int maxVal=max(sel_1,sel_2);
		maxVal=max(maxVal,sel_3);
		return maxVal;
	} else {
		//只有一个选择
		return lcs(s1,i+1,s2,j+1)+1;
	}
}
int main() {
	string s1,s2;
	cin>>s1>>s2;
	int res= lcs(s1,0,s2,0);
	cout<<res;
	return 0;
}

Cuando la longitud de la cadena es grande, la cantidad de cálculo basado en la recursividad será grande.El problema es que hay una gran cantidad de subproblemas superpuestos en el algoritmo recursivo.

2.3 Subproblemas superpuestos

Dibujar un árbol recursivo muestra claramente la existencia de subproblemas superpuestos.

48.png

Y se puede ver sel_1que las sel_2ramas incluyen sel_3ramas, y se puede usar un esquema de almacenamiento en caché para resolver subproblemas superpuestos en recursividad, de modo que los subproblemas superpuestos solo se calculan una vez. El código completo es el siguiente:

#include <iostream>
#include <map>
using namespace std;
//缓存
map<pair<int,int>,int> cache;
int lcs(string s1,int i,string s2,int j) {
	if(i==s1.size() || j==s2.size())return 0;
	pair<int,int> p= {i,j};
	if (cache[p] ) {
		return cache[p];
	}
	if(s1[i]!=s2[j]) {
		//有 3 种选择
		int sel_1=lcs(s1,i,s2,j+1);
		int sel_2=lcs(s1,i+1,s2,j);
		cache[p]=max(sel_1,sel_2);;
	} else {
		//只有一个选择
		cache[p]=lcs(s1,i+1,s2,j+1)+1;
	}
	return 	cache[p];
}
int main() {
	string s1,s2;
	cin>>s1>>s2;
	int res= lcs(s1,0,s2,0);
	cout<<res;
	return 0;
}

El rendimiento de la implementación recursiva no es impresionante y el nivel de código también es un poco engorroso. Similar a este problema de encontrar el valor máximo, puede intentar usar la programación dinámica para lograrlo.

2.4 Programación Dinámica

La idea de la resolución recursiva de problemas es de arriba a abajo. El llamado de arriba a abajo se refiere a dejar de lado los problemas de mayor escala primero y luego retroceder para encontrar la solución al gran problema después de los subproblemas de menor escala. están resueltos. El proceso de solución se puede ver claramente a través del árbol recursivo publicado anteriormente.

La idea de la programación dinámica es de abajo hacia arriba y se basa en la enumeración. Registre la solución de cada subproblema y, finalmente, obtenga la solución del problema más grande. Por supuesto, se requiere que los pequeños problemas sean independientes y óptimos.

No importa de arriba hacia abajo o de abajo hacia arriba, la esencia es encontrar la respuesta al gran problema después de conocer la respuesta a la subpregunta. El algoritmo de programación dinámica es sencillo y la recursividad es una solución indirecta.

Ahora tome la subsecuencia común más larga de una cadena como ejemplo para explicar el proceso de solución de la programación dinámica.

Cree dpuna matriz para registrar las soluciones de todos los subproblemas, similar al caché implementado por recursividad. En lo que respecta a este problema, dpse trata de una matriz bidimensional. En teoría, se Aderiva de By luego Bse deriva de C... El dominio del problema tiene que ver con la conclusión de la derivación final C. Las conclusiones de la derivación histórica utilizadas antes pueden en realidad ser almacenado sin almacenamiento. Un poco similar a "desagradecido", por lo que dpla matriz se puede comprimir.

  • Construya dpuna matriz bidimensional. Primero inicialice los valores de la primera fila y la primera columna de la matriz 0. La derivación debe tener una fuente, y aquí 0está la fuente.

    When s1=""、s2="a……"o when s1="a……"、s2=""o s1=""、s2=""when puede considerarse el valor de la subsecuencia común más larga 0.

49.png

  • Como se muestra en la figura, deje que los caracteres en i=1、j=1la s1[i]和s2[j]posición de comparación obviamente no ksean tiguales. La recursión es para ver cuántos subproblemas se pueden seleccionar en la parte posterior (aún no resueltos), y la programación dinámica es para ver cuántos subproblemas en el frente (ya resueltos) afectarán el subproblema actual. Para el puesto actual, hay un puesto que le afecta 3. La posición del área amarilla está marcada en la siguiente figura.

    1Las coordenadas de ubicación son (i,j-1). Indica el valor de la subsecuencia común más larga cuando s1ambas están presentes ky s2ausentes .t

    2Las coordenadas de ubicación son (i-1,j-1). Indica el valor de la subsecuencia común más larga cuando s1existe none ky s2none .t

    3Las coordenadas de ubicación son (i-1,j). Un valor que representa la subsecuencia común más larga de s1ninguno ky algunoss2 de ellos .t

50.png

La posición se puede descartar 3, y luego se puede encontrar el valor máximo en la posición 1y la posición 2.

51.png

  • i=1Sin cambios, cambiado jal valor. La comparación s1[i]y s2[j]el valor mediano a lo largo del camino no son iguales.De acuerdo con el análisis anterior, es fácil completar dpel valor.

52.png

  • Mover i=2, restablecer j=1y mover j.

    iSe ha analizado el problema cuando jno es igual al personaje en la posición.

    Como se muestra en la figura a continuación, i=2,j=2cuando s[i]和s[j]los valores de son iguales, ¿cuál debería ser la posición principal que afecta este valor de posición?

54.png

Igual, obviamente la subsecuencia común más larga aumentará 1, la pregunta es ¿qué valor previo al subproblema agregar 1?

​ De hecho, solo necesita agregar al valor de la posición en el área amarilla a continuación 1, que s1和s2indica acuando no hay ninguno.

56.png

  • dpDe acuerdo con el principio de análisis anterior, se puede completar todo el formulario.

58.png

Implementación de codificación:

#include <iostream>
#include <map>
using namespace std;
int dp[100][100]= {0};
void lcs(string s1,string s2) {
	//初始化动态规划表
	for(int i=0; i<s2.size(); i++)
		dp[0][i]=0;
	for(int i=0; i<s1.size(); i++)
		dp[i][0]=0;

	for(int i=1; i<=s1.size(); i++) {
		for(int j=1; j<=s2.size(); j++)
			if(s1[i-1]==s2[j-1]) {
				//相等
				dp[i][j]=dp[i-1][j-1]+1;
			} else {
				dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
			}
	}
}
int main() {
	string s1,s2;
	cin>>s1>>s2;
	lcs(s1,s2);
	for(int i=0; i<=s1.size(); i++) {
		for(int j=0; j<=s2.size(); j++) {
			cout<<dp[i][j]<<"\t";
		}
		cout<<endl;
	}
	cout<<"最长公共子序列:"<<endl;
	int res=dp[s1.size()][s2.size()];
	cout<<res<<endl;
	return 0;
}

Resultados de la prueba:

59.png

4. Resumen

La subsecuencia común más larga es muy representativa, analizar el proceso de implementación basado en recursividad y programación dinámica puede ayudarnos a comprender y resolver este tipo de problemas.

Supongo que te gusta

Origin blog.csdn.net/y6123236/article/details/132269102
Recomendado
Clasificación