3335 - Total Characters in String After Transformations I
info
- 문제 보기: 3335 - Total Characters in String After Transformations I
- 소요 시간: 41분 57초
- 풀이 언어:
java
- 체감 난이도: 3️⃣
- 리뷰 횟수: ✅
풀이 키워드
스포주의
DP
풀이 코드
info
- 메모리: 45500 KB
- 시간: 97 ms
class Solution {
final int MOD = 1_000_000_007;
public int lengthAfterTransformations(String s, int t) {
int[] dp = new int[26];
int[] nxt = new int[26];
// init: if t == 0 then length = 1
for (int ch = 0; ch < 26; ++ch) dp[ch] = 1;
while (t-- > 0) {
for (int ch = 0; ch < 26; ++ch) {
if (ch == 25) { // cnt('z') <- cnt('a') + cnt('b')
nxt[ch] = (dp[0] + dp[1]) % MOD;
} else {
nxt[ch] = dp[ch+1];
}
}
int[] tmp = dp;
dp = nxt;
nxt = tmp;
}
long ans = 0;
for (char ch : s.toCharArray()) {
ans = (ans + dp[ch-'a']) % MOD;
}
return (int) ans;
}
}
풀이 해설
z를 제외한 나머지 알파벳은 +1씩 값이 변경되고 (ex. a -> b, x -> y)
z는 ab로 변경된다는 조건 하에, 주어진 문자열을 t만큼 변경해서 그 길이를 구하는 문제이다.
길이 확장 조건이 동적이라서 특정 공식으로 도출하기 어렵다.
caution
응 아니야
다음의 내용들은 오답에서 정답까지의 과정을 다루고 있다. (재실수 방지 목적)
❌ 이진 확장 -> WA
a ~ z까지는 글자수 변동 없이 내용만 바뀌고
t의 26주기 시점에서만 z -> ab로 글자수가 2배 확장되는 점을 고려해서 이진 확장인가 싶었다.
따라서 s의 한 글자씩 순회하면서 해당 글자가 t 이후에 얼마나 확장될지를 공식화했고
1 << n/26
으로 구해진다고 생각했다. (단, n = t + 'a'로부터의 오프셋)
하지만 잘 생각해보면 z -> aa 가 아니고 z -> ab라서 확장 주기가 일정치 않다.
z는 +26 시점에서 ab -> zab로 b만 확장되어 2글자가 아닌 3글자가 되고
+27이 되어야 ab -> abbc로 4글자가 된다. 하지만 이렇게 또 서로 다른 오프셋때문에 다음 주기에도 확장 타이밍이 글자마다 제각각이 된다.
마찬가지로 기존 도출된 공식으로 계산하면 y는 2바퀴째인 +52 시점부터 오답이 나옴을 확인할 수 있다.
👉 결론적으로, 해당 접근은 틀렸다.
WA 코드
import java.math.*;
class Solution {
public int lengthAfterTransformations(String s, int t) {
// a(1) -> ab(2) -> abbc(4) -> abbcbccd(8)
// 2^0 -> 2^1 -> 2^2 -> 2^3
// 0 -> 26 -> 26*2 -> 26*3
final int MOD = 1_000_000_007;
long sum = 0;
for (char ch : s.toCharArray()) {
int offset = ch - 'a'; // offset: required t to transform 'a' into ch
int n = t + offset;
int res = BigInteger.ONE.shiftLeft(n/26).mod(BigInteger.valueOf(MOD)).intValue();
sum += res % MOD;
}
return (int) sum;
}
}
"jqktcurgdvlibczdsvnsg"
7517
❌ 하향식 DP -> TLE1
공식화가 안되는데다 캐싱의 여지가 있으니 차선책인 DP로 접근했다.
메모이제이션으로 각 알파벳의 t별 길이를 캐싱했는데 함수 호출 오버헤드 때문인지 제약조건의 상한에서 TLE가 터졌다.
TLE1 코드
class Solution {
final int MOD = 1_000_000_007;
int memo[][] = new int[26][100_001];
public long dp(int ch, int t) {
if (t == 0) return 1;
if (memo[ch][t] != 0) return memo[ch][t];
long res = 0L;
if (ch == 25) res = (dp(0, t-1) + dp(1, t-1)) % MOD;
else res = dp(ch+1, t-1) % MOD;
return memo[ch][t] = (int) res;
}
public int lengthAfterTransformations(String s, int t) {
long ans = 0L;
for (char ch : s.toCharArray()) ans = (ans + dp(ch-'a', t)) % MOD;
return (int) ans;
}
}
{a만으로 구성된 길이 10만의 문자열}
100000
❌ 상향식 DP -> TLE2
하향식으로 TLE를 받아서 호출 오버헤드가 없는 상향식으로 고쳤는데 역시나 TLE였다.
왜지?? 하고 정보를 찾아보니 메모이제이션을 2차원으로 정의한게 원인이었다.
C/C++에선 그냥 스택 메모리에 할당하면 다 충분히 빠른데
자바는 2차원 배열이 객체배열 + 내부배열 구조이고, 힙 메모리에 위치해있어 더 느리다.
그리고 메모리 접근 지역성의 관점에서도 CPU caching + prefetch가 들어가는 1D는 2D와 아주 큰 퍼포먼스 차이를 낼 수 있다.
그러므로 자바에서의 dp 연산은
⭐2D 배열을 n개의 1D 배열로 치환⭐하여
각 배열을 주소로 스왑해서 쓰는게 훨 빠르다는 결론.
하지만 1D나 2D나 시간복잡도는 동일하다.
둘 다 이다. 😥
보통 알고리즘 자체의 복잡도 선에서 정답 컷을 내는데 메모리 병목 차이로 정답이 갈리는게 다소 읭스럽긴 하다.
TLE2 코드
class Solution {
final int MOD = 1_000_000_007;
long dp[][] = new long[26][100_001];
public int lengthAfterTransformations(String s, int t) {
for (int i = 0; i < 26; ++i) dp[i][0] = 1;
for (int j = 1; j < t+1; ++j) {
for (int i = 0; i < 26; ++i) {
if (i == 25) dp[i][j] = (dp[0][j-1] + dp[1][j-1]) % MOD;
else dp[i][j] = dp[i+1][j-1];
}
}
long ans = 0L;
for (char ch : s.toCharArray()) ans = (ans + dp[ch-'a'][t]) % MOD;
return (int) ans;
}
}
"rssjmowborzlmeqqqobyqcejqloqicrzuioahtgvdfptpdiofqnnximowhqovyqxhclnokkvqjwsrftkyerwnbecbmanninbgwwkglezhocqjwgzlqddzojmiohtkqdrmjiowbfvtzrqtzmkcfmivdiqqqueacibannstgtvqsijncxchwbfdvmqlzfinhijodevmabkcubwzwhyytfbveefesyayesszwgmzyurpzyjohvywngbtokwequjxcjyoeyoarnlopivfuenhzounucgeuawoxgqvixmlpjmqbvmtunksququawvbmwzyftijgkmkzxhqjleoblkyfdcycsnzvriygckuaxwendcmgfbrnggsozmhxmdpiuuopgfnxekoyfnlhzctdkfdwzbrdjxrvqqnasaqvtdrcqcgfdmpxakujbsrmcintncablupxhtzsqivaetfcufmxsmugzzmkhlachvgemgwrrhevzzpsqjvhgyalfpnxbfxhyglxfhfvbpbypyourftcowtnsykcadbqufdzihzsjylccmjohjajyejuwnosiovynbosxewxhksoszumnfmapjuvkmenmtanjbrldmfrlvoofdtluktbwwzavxxsbcrsvpbzbpqdutasuiowhmtgowvghndncmhrusxgfrjrcrqwrvtlvpxdfydeqyzowxxgrpgrzrotpaxhcrrpoeuomoxklvgdfdnhbgwpolpwfuscyvlkpbamwcbmqoofplzevjkspktcjanfglhygtfbqxagnqtpgfhpkqgnywzvckdbbyfmyklxpwqsslctqerxgxwumoxvetprjrmttxdjyarbjucsaanhmxknqlnanqzmzearndgslboqubuxmvhwcrnqhuoiebalcsaektkbzwztkaluhecfpwvzdzjyfzevxroclrdqwlxjxuadsntsbidptxrlwjyzubzflxllsntjcbxexciujwcqbljistpyaepabpophyhxhzfgbavxwsaodglknjgnbqspreylgaiziqzhyeafdubhvtfgflkcgwhsocjhywnqjvqqwxdoymzfskibejnwarqpjyootzvqfgqzavutozkzebfvbagtsgpkreraifogvyaxjrukcxnwurpnpmmyqcsegkiakepsvxgpillpzsuonmixasdjbpzmblkgjaiwmlmqoxaameqrcawskfqsjccxeqlwuxczhtxwgcojiofqkttffaflpuihwpg"
3434
메모
- 이 문제도 릿코 댓글이 웃김 ㅋㅋ
- WA -> TLE1 -> TLE2 거치면서 그라데이션으로 혈압이 올랐다.
- C#도 잘만 2D로 통과하는데 언어 스펙에서 이렇게 구린거 처음 봄;
- 더 deep하게 보려면 JIT 디버깅 (2D 구조가 JIT 최적화를 어렵게 한다고 함) + JMH 벤치마킹 비교까지 들어가야 한다.
t
범위때문에 DP 배제해놓고 봤는데 다시 생각하니 미만이니까 괜찮았던 듯?