[LeetCode] Scramble String(일반적이지 않은 DP 해결)

Implement regular expression matching with support for  '.'  and  '*' .
'.' Matches any single character.
'*' Matches zero or more of the preceding element.

The matching should cover the entire input string (not partial).

The function prototype should be:
bool isMatch(const char *s, const char *p)

Some examples:
isMatch("aa","a") → false
isMatch("aa","aa") → true
isMatch("aaa","aa") → false
isMatch("aa", "a*") → true
isMatch("aa", ".*") → true
isMatch("ab", ".*") → true
isMatch("aab", "c*a*b") → true

이 문제의 귀착적인 사고방식은 매우 뚜렷하고 대체적인 사고방식은 다음과 같다.
s1과 s2에 대해 각각 두 개의 하위 문자열로 나뉜다.각각 s1 라고 부른다left, s1_right, s2_left, s2right.만약 s1과 s2가 scramble라면 두 가지 상황만 있을 뿐이다.
1) s1_left와 s2left는 scramble이고 s1left와 s2left는 scramble입니다.
2) s1_left와 s2right는 scramble이고 s1right 및 s2left는 scramble입니다.
여기에 귀속되면 하위 문제를 반복해서 풀 수 있다는 점을 감안하면 DP를 사용할 수밖에 없다.일반적인 DP 방법을 사용하려면 3차원 DP가 필요합니다: dp[i][j][k]는 s1이 i에서 시작하고 s2가 j에서 시작하며 길이가 k인 두 개의 하위 문자열이 scramble인지 여부를 나타냅니다.
그러나 상기 해법은 매우 번거롭고 사실 이런 3차원 DP표에는 여전히 불필요한 상태가 저장되어 있다.스스로 비상식적인 DP 구해 방법을 생각해 냈는데, 3차원 DP 시계를 해시 시계로 대체하여 불필요한 상태에 대한 저장을 줄일 수 있다는 것을 발견하였다.그리고 이곳의 DP는 사실상 귀착적인 사고방식과 같이 자정향하는 것이지 이른바 진정한 자저상향의 DP가 아니다.
여기에 하위 문제의 해답 결과를 기록하는 데이터 구조는Map입니다. 앞에서 사실 게으름을 피웠습니다. 이론적으로 Pair과 비슷한 것이어야 합니다. 즉, 임의의 두 문자열을pair로 보고 scramble의 결과인지 기록해야 합니다.그런데 만약에 정말 수동으로 클래스를 쓰고 equals를 덮어쓰는 방법이 있다면 너무 귀찮아서 여기서 게으름을 피우고Pair의 형식을s1@s2, 특수 문자열로 기록합니다.
또한 두 문자열의 길이가 같지 않으면 바로false로 되돌아갈 수 있는 몇 개의 빠른 귀속 출구를 제공합니다.다른 하나는 두 문자열의 내용이 같으면true로 바로 되돌아갈 수 있다는 것이다.
	// Memo for dp.
	private Map<String, Boolean> dp = new HashMap<String, Boolean>();
	public boolean isScramble(String s1, String s2) {
		String key = s1 + "@" + s2;
		if (dp.containsKey(key)) {
			return dp.get(key);
		}

		// Two quick recursion exits.
		if (s1.length() != s2.length()) {
			dp.put(key, false);
			return false;
		}
		if (s1.equals(s2)) {
			dp.put(key, true);
			return true;
		}

		// Partition and match recursively.
		for (int i = 1; i < s1.length(); ++i) {
			for (int j = 1; j < s2.length(); ++j) {
				// i and j are the partition indexes.
				String s1_left = s1.substring(0, i);
				String s1_right = s1.substring(i);
				String s2_left = s2.substring(0, j);
				String s2_right = s2.substring(j);

				if (isScramble2(s1_left, s2_right)
						&& isScramble2(s1_right, s2_left)
						|| isScramble2(s1_left, s2_left)
						&& isScramble2(s1_right, s2_right)) {
					return true;
				}
			}
		}

		dp.put(key, false);
		return false;
	}

사실 이 문제는 DP 외에도 사용할 수 있다
귀속+가지치기 방법.간단한 귀환은 TLE를 초래하지만, 효과적인 가지치기는 때로DP보다 더 좋은 효과를 얻을 수 있다.
이곳의 가지치기 조건은 괜찮다
간단하게 설정하면 모든 문자의 ASCII 값의 합은 반드시 같아야 한다. 이것은 scramble의 충분한 조건이 된다.코드는 다음과 같다.
	public boolean isScramble2(String s1, String s2) {
		// Two quick recursion exits.
		if (s1.length() != s2.length())
			return false;
		if (s1.equals(s2))
			return true;

		// Prune candidates here to reduce candidate check times.
		int sum = 0;
		for (int i = 0; i < s1.length(); i++) {
			sum += s1.charAt(i) - 'a';
			sum -= s2.charAt(i) - 'a';
		}
		if (sum != 0)
			return false;

		// Partition and match recursively.
		for (int i = 1; i < s1.length(); ++i) {
			for (int j = 1; j < s2.length(); ++j) {
				// i and j are the partition indexes.
				String s1_left = s1.substring(0, i);
				String s1_right = s1.substring(i);
				String s2_left = s2.substring(0, j);
				String s2_right = s2.substring(j);

				if (isScramble2(s1_left, s2_right)
						&& isScramble2(s1_right, s2_left)
						|| isScramble2(s1_left, s2_left)
						&& isScramble2(s1_right, s2_right)) {
					return true;
				}
			}
		}

		return false;
	}

이 문제를 총괄해 보자.일반적으로 가장 좋은 문제를 구하는 데는 일반적으로 두 가지 사고방식이 있다.
1) 충분한 행운으로 탐욕성을 발견할 수 있다면 탐욕법을 우선선택해야 한다.(이 문제는 분명 욕심이 없어서 소홀히 할 수 있다.)
2) 그렇지 않으면 귀속 성질을 찾아 귀속 구해를 시도한다.
2) 여기서 먼저 하위 문제가 반복적으로 풀릴지 여부를 본다.만약 중복된 하위 문제 구해가 존재한다면 사실 세 가지 선택이 있을 수 있다.
1) DP는 일반적인 사고방식이다.
2)귀속+가지치기(당연히DP를 사용할 때도 가지치기가 가능함).
3) DP와 귀속 결합: 위에서 아래로 귀속 구해를 구하는 김에 비망록에 부딪힌 하위 문제의 결과를 기록한다.
이 문제는 운행 시간을 보면 귀속+가지치기의 효과가 가장 좋은 것을 발견했다. 주로 이 문제의 귀속 정도가 너무 깊다는 것이다...

좋은 웹페이지 즐겨찾기