Strings in C++: Roman to Integer, Integer to Roman, and Factorial of Large Numbers
This chapter focuses on advanced string and array manipulation techniques in C++. It addresses two major categories of problems: Roman Numeral Conversions (positional number systems, lookahead strategies, and structural decomposition) and Massive Numerical Computations (handling integer overflow using arbitrary-precision dynamic array storage).
1. Fundamentals of Roman Numerals & Problem Constraints
Roman numerals are a non-positional numeral system represented by specific alphabetic characters. Each character corresponds to a fixed integer value.
Standard Character Mapping
Note: A common shorthand sequence to memorize character order from smallest to largest is: I, V, X, L, C, D, M ().
Core Constraints & Guarantees
In standard algorithmic problems (such as LeetCode 13 & 12), inputs and expected outputs are strictly bounded:
- Valid Range:
- This upper bound guarantees that we never need symbols beyond
M() or complex bar-notation for values .
2. Roman to Integer: Core Mental Model & Algorithm
Basic Addition vs. Subtraction Rule
In standard non-subtractive cases, values are written from largest to smallest, and we simply sum them up:
IIILXXCCCXXXV
However, when a smaller value symbol appears before a larger value symbol, it indicates a subtractive combination:
IV(not )IXXLCD
Lookahead Decision Strategy
To convert a Roman numeral string to an integer:
- Traverse the string character by character from index
i = 0up tos.size() - 2. - Compare the numeric value of the current character
s[i]with the numeric value of the next characters[i + 1]:- Case A (
Value(s[i]) < Value(s[i + 1])): The current character is subtractive. SubtractValue(s[i])from the cumulative total (sum -= Value(s[i])). - Case B (
Value(s[i]) >= Value(s[i + 1])): The current character is additive. AddValue(s[i])to the cumulative total (sum += Value(s[i])).
- Case A (
- After the loop terminates, the final character
s[s.size() - 1]has no lookahead neighbor. Always add its value directly tosum.
Lookahead Decision Workflow
3. Roman to Integer: Complete C++ Implementation
Code Implementation
#include <iostream>
#include <string>
using namespace std;
// Helper function to map individual Roman characters to their integer values
int number(char c) {
if (c == 'I') return 1;
if (c == 'V') return 5;
if (c == 'X') return 10;
if (c == 'L') return 50;
if (c == 'C') return 100;
if (c == 'D') return 500;
return 1000; // 'M'
}
int romanToInt(string s) {
int sum = 0;
int index = 0;
int n = s.size();
// Iterate up to the second-to-last character
while (index < n - 1) {
if (number(s[index]) < number(s[index + 1])) {
sum -= number(s[index]);
} else {
sum += number(s[index]);
}
index++;
}
// Always add the last character's value directly
sum += number(s[n - 1]);
return sum;
}
int main() {
string str = "XLIII";
cout << "Integer value of " << str << " is: " << romanToInt(str) << endl; // Output: 43
return 0;
}
Dry-Run Walkthrough (s = "XLIII", Length = )
- Initial State:
sum = 0,index = 0
Complexity Analysis
- Time Complexity: , where is the length of the string. The lookahead logic processes each character in time.
- Space Complexity: auxiliary space, as no extra dynamic memory is allocated.
4. Integer to Roman: Positional Breakdown & Framework
To convert an integer () to a Roman numeral string, decompose into its decimal place values: Thousands, Hundreds, Tens, and Units.
Positional Decomposition Principle
Consider :
Each place value is mapped independently to its corresponding Roman representation:
- Combined Result:
"MCCXLVIII"
Lookup Framework Table
By pre-defining lookup arrays for all possible place values, conversion reduces to a sequence of direct indexed lookups:
Conceptual Solution Paradigm
Using static lookup arrays for each position allows constant time lookup:
string ones[] = {"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"};
string tens[] = {"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"};
string hundreds[] = {"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"};
string thousands[]= {"", "M", "MM", "MMM"};
string intToRoman(int num) {
return thousands[num / 1000] +
hundreds[(num % 1000) / 100] +
tens[(num % 100) / 10] +
ones[num % 10];
}
- Time Complexity:
- Space Complexity:
5. The Integer Overflow Challenge in Large Factorials
Why Standard Data Types Fail
Calculating factorials () using primitive integer types fails rapidly due to fixed bit-width storage boundaries.
Standard 32-bit Signed Integer Range:
-2^31 to +(2^31 - 1) => -2,147,483,648 to +2,147,483,647
Bit Distribution in 32-bit Primitive Integers
1 bit is reserved for the sign, leaving 31 bits for value magnitude:
[ Sign Bit (1 bit) ] [ Magnitude Bits (31 bits) ]
Maximum positive capacity .
Growth Rate of Factorials
When exceeds the container limit, arithmetic wraparound occurs, producing incorrect negative numbers or truncated zero outputs.
6. Representing Large Numbers via Arrays/Vectors
To bypass hardware register limitations, we represent massive numbers using dynamic arrays (std::vector<int>).
Array Storage Architecture
Instead of storing as a scalar integer, each single-digit value ( through ) is stored in its own array slot:
Number: 40320
Array Representation:
Index: [ 0 ] [ 1 ] [ 2 ] [ 3 ] [ 4 ]
Digit: 0 2 3 0 4 (Stored in REVERSE order)
Why Store Digits in Reverse Order?
When multiplying a multi-digit number by a scalar multiplier , carry operations propagate from the least significant digit (LSD) to the most significant digit (MSD):
In a standard C++ std::vector:
- Inserting at the end via
vector::push_back()runs in amortized time. - Inserting at the beginning via
vector::insert(begin(), val)runs in time because all existing elements must shift right.
By storing the number in reverse order (index 0 holds the units digit):
- New digits generated by residual carries are pushed directly to the back of the vector in time.
- Once all multiplications are complete, the entire vector is reversed once at the very end to produce the correct final string/array output.
7. Step-by-Step Multiplication & Carry Workflow
To compute , initialize the array with a single element [1]. Iteratively multiply the contents of the array by .
Primary Loop Algorithm (Multiplying Vector by Scalar )
- Initialize
carry = 0. - Iterate through all existing elements
ans[i]fromi = 0up tosize - 1:- Compute raw product:
result = (ans[i] * x) + carry - Store single-digit remainder in slot
i:ans[i] = result % 10 - Calculate new carry:
carry = result / 10
- Compute raw product:
- Process remaining multi-digit
carry(ifcarry > 0after completing element-wise operations):- While
carry > 0:- Append its unit digit to the array:
ans.push_back(carry % 10) - Reduce carry:
carry = carry / 10
- Append its unit digit to the array:
- While
Multi-Digit Carry Propagation Trace
8. Complete C++ Implementation: Factorial of Large Numbers
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// Computes factorial of large numbers and returns vector of digits
vector<int> factorial(int n) {
// Vector stores digits in REVERSE order
vector<int> ans;
ans.push_back(1); // Base case: 0! or 1! = 1
// Multiply ans by every number x from 2 up to n
for (int x = 2; x <= n; x++) {
int carry = 0;
int size = ans.size();
// Perform element-wise multiplication
for (int i = 0; i < size; i++) {
int result = (ans[i] * x) + carry;
ans[i] = result % 10; // Store unit digit
carry = result / 10; // Pass remainder as carry
}
// Process residual multi-digit carry
while (carry > 0) {
ans.push_back(carry % 10);
carry /= 10;
}
}
// Reverse array to restore correct Most Significant Digit -> Least Significant Digit order
reverse(ans.begin(), ans.end());
return ans;
}
int main() {
int N = 50;
vector<int> result = factorial(N);
cout << N << "! = ";
for (int digit : result) {
cout << digit;
}
cout << endl;
return 0;
}
Complete Execution Trace ()
- Initialize:
ans = [1]
Complexity Analysis
- Time Complexity: , where is the input scalar and is the number of digits in . By Stirling's approximation, . Hence, total execution time runs in , easily executing well under seconds for .
- Space Complexity: auxiliary space to store the output digits.
9. Real-World Debugging, Pitfalls, & Industry Mindset
Common Bugs & Edge Cases
- Infinite Loop via Missing Counter Update:
C++
// INCORRECT while (index < s.size() - 1) { if (...) sum -= ...; // Missing index++ leads to Time Limit Exceeded (TLE) } - Improper Multi-Digit Carry Processing:
Assuming
carryis always a single-digit number () causes severe bugs when multiplying against large dynamic inputs:C++// INCORRECT if (carry > 0) ans.push_back(carry); // CORRECT: Process all digits of carry sequentially while (carry > 0) { ans.push_back(carry % 10); carry /= 10; }
Debugging Mindset in Technical Engineering
Instructor Insight: Multiple compilation errors or Wrong Answers (WA) during initial submissions are a natural, essential part of the learning journey. Real production debugging involves systematically identifying boundary conditions (e.g., zero factorials, multi-digit carries, container boundary limits) and step-by-step trace verification.
10. Summary and Key Takeaways
- Roman Numeral Structure: Standard conversion uses a lookahead strategy comparing
s[i]withs[i + 1]. Ifs[i] < s[i + 1], subtracts[i]; otherwise, adds[i]. - Positional Integer Conversion: Decimal breakdown into thousands, hundreds, tens, and units maps directly to fixed lookup tables.
- Integer Overflow Threshold: 32-bit signed integers overflow beyond (). 64-bit integers overflow beyond .
- Dynamic Array Digit Storage: Large values are stored digit-by-digit inside custom dynamic arrays or vectors to achieve arbitrary-precision computation.
- Reverse Digit Orientation: Digits are processed in reverse order (LSD at index
0) to allow dynamic growth at the tail via.push_back(). - Residual Carry Propagation: After processing element-wise digit products, remaining carry values must be processed iteratively using
while (carry > 0)loops to append every single digit correctly. - Final Output Transformation: Once all arithmetic iterations are completed, the vector must be reversed to output the correct most-significant-to-least-significant digit sequence.