Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

I am working on bettering my C# skills, and recently wrote this program to solve a projecteuler.net problem:

A palindromic number reads the same both ways. The largest palindrome made from the product of two 2-digit numbers is 9009 = 91 * 99. Find the largest palindrome made from the product of two 3-digit numbers.

The code works and produced the correct answer, but I'm looking to avoid bad code and use best practices. Any critique of changes to make this more efficient would be appreciated! (Also, if there are any WTF's in there, please point them out!)

using System;
using System.Collections.Generic;
using System.Text;

namespace NumericPalindrome {
    class Program
    {
        static void Main(string[] args)
        {
            long result = 0;

            for (int i = 100; i < 1000; i++)
            {
                for (int k = 100; k < 1000; k++)
                {
                    if (IsPalindrome(i * k))
                        if ((i * k) > result)
                            result = i * k;
                }
            }

            Console.WriteLine(result);
        }



        static bool IsPalindrome(long testNumber)
        {
            List<int> list = new List<int>();

            //Break testNumber into an integer list of its digits
            while (testNumber >= 1)
            {
                list.Add((int)(testNumber % 10));
                testNumber /= 10;
            }

            List<int> reverseList = new List<int>(list);
            reverseList.Reverse();

            bool palindrome = true;

            for (int i = 0; i < list.Count; i++)
            {
                if (list[i] != reverseList[i])
                    palindrome = false;
            }

            return palindrome;
        }
    }
}
share|improve this question
add comment

6 Answers

up vote 10 down vote accepted
  • First of all, add benchmarking to your program: before anything else, record the current time. Right before the program terminates, subtract the recorded time from the current time and display the difference. Run it and record the time it takes. Then, implement the following optimizations, and see how much time you save.

  • In your first nested two for-loops you are not taking advantage of the fact that A*B = B*A. So, you are doing about twice the work that is necessary. Cut it in half! (I presume that once you realize the idea, you can figure out how; if not, ask me.)

  • The three repeated calculations of i * k are kind of ugly. Why don't you compute the product once, and use it 3 times? This is not a performance issue, just an aesthetic one.

  • To check if the list is a palindrome, you do not need to make a copy of it and reverse. You can do something which far more efficient, and even easier to implement: loop from the start of the list until the middle, and compare the digit at that position with the corresponding digit counting from the end of the list. If the list contains an odd number of digits, ignore the one in the middle. (Again, I presume you can figure it out; if not, ask me.)

  • You can return false from within the loop immediately upon finding that the number is not a palindrome; it is totally unnecessary to keep looping until you have checked all the digits.

  • If you really-really care about performance, you can start doing hacks, like making the list a static member variable of your program, allocating once, and clearing it each time before using it, instead of reallocating a new list each time. You can also find an algorithm which can determine whether a number is a palindrome without constructing a list. It is ugly, but it works; look here: http://stackoverflow.com/questions/199184/how-do-i-check-if-a-number-is-a-palindrome

share|improve this answer
    
Great answer with a lot of improvements. Thanks, Mike! –  Jim Jan 10 '12 at 20:07
    
Glad to be of help, Jim! Another happy customer! –  Mike Nakis Jan 10 '12 at 20:11
add comment

I do not do C# but I can offer another way of approaching the problem:

As our range of numbers is \$[100\cdot100, 999\cdot999] = [10000, 998001]\$, assume a palindromic number on the form \$abccba\$.

The largest \$abccba < 998001\$ is \$k = abccba = 997799\$. We need to determine if \$k\$ can be written as the product of two 3-digit numbers. Using a prime table (or generate one with a sieve at program start) see if any prime number \$p\$ where \$\min\left(\frac{k}{100}, 999\right) \lt p\le k\$ evenly divides \$k\$.

If you can find such a prime then we know that \$\frac{k}{p} \lt 100 \$ and any remaining factors are too small or that \$p \gt 999\$ and we have a factor that is too large. So we can discard \$k\$ and decrement \$c\$ by one to form the next smaller palindrome, eg. \$k=996699\$, and keep trying smaller palindromes.

If you reach \$p\lt \min\left(\frac{k}{100}, 999\right) \$ without finding any prime that evenly divides \$k\$ we need to convince ourselves that the remaining factors can be grouped so both are three digits.

There are a few ways to convince ourselves that the above. One way is trial division by simply trying all divisors \$d\$ such that \$100\le d \le \max\left(\frac{k}{100}, 999\right)\$ and checking what the other factor is.

Another more elegant way is to use the fact that we know that there is no prime factor larger than \$p\le\min\left(\frac{k}{100}, 999\right)\$ and find the largest prime factor \$p_{max}\le\min\left(\frac{k}{100}, 999\right)\$ and let \$r_i = \frac{k}{p_{max}}\$.

Let the prime factors of \$r_i\$ be \$p_i\$ and let \$Q=\prod_{\forall i:p_i<\frac{999}{p_{max}}}p_i\$. At this point you have the largest prime factor of \$k\$ as \$p_{max}\$ and all prime factors of \$r_i=\frac{k}{p_{max}}\$ that are too big to be together with \$p_{max}\$ multiplied together as \$Q\$.

Now, factor \$\frac{r_i}{Q}\$ into primes \$P_k\$. Each of the \$P_k\$ primes has to be multiplied into either \$Q\$ or \$p_{max}\$. Iteratively try all combinations (and exit early for impossible branches) until you find one that is satisfactory or retry next smallest palindrome if no combination was found.

Note: This may well be slower than trial multiplication but it's another way to approach the problem and it may give birth to other ideas.

share|improve this answer
    
Not sure if this is faster or slower, but it's another approach. :) –  Emily L. Jun 26 at 17:10
add comment

Considering other answers, here is my updated solution :

class P004
{
    private const int NumberOfDigits = 3;
    private readonly static int UpperBound = (int)Math.Pow(10, NumberOfDigits) - 1;
    private readonly static int LowerBound = (int)Math.Pow(10, NumberOfDigits -1);

    static void Main(string[] args)
    {
        int currentMax = 0;
        for (int i = UpperBound; i >= LowerBound; i--)
        {
            for (int k = i - 1; k >= currentMax / i; k--)
            {
                int num = i * k;
                if (IsPalindrome(num))
                {
                    if (currentMax < num) 
                    {
                        currentMax = num;
                    }
                }
            }
        }

        Console.WriteLine(currentMax);
        Console.ReadLine();
    }

    static bool IsPalindrome(long testNumber)
    {
        var n = testNumber;
        var rev = 0L;
        while (testNumber > 0)
        {
            var dig = testNumber % 10;
            rev = rev * 10 + dig;
            testNumber = testNumber / 10;
        }
        return n == rev;
    }

}

Don't know if it helps, but here is my solution, in a more "Linq" way :

    private const int NumberOfDigits = 3;
    static void Main(string[] args)
    {
        var r = from x in NumbersWithNDigits(NumberOfDigits, (int)Math.Pow(10, NumberOfDigits - 1))
                from y in NumbersWithNDigits(NumberOfDigits, x + 1) 
                let p = x*y
                let pStr = p.ToString()
                where pStr == new string(pStr.Reverse().ToArray())
                select new { x,y, p };

        Console.WriteLine(r.Max(s=>s.p));

        Console.ReadLine();
    }


    static IEnumerable<int> NumbersWithNDigits(int n, int lowerBound)
    {
        int upperBound =(int) Math.Pow(10,n) -1;

        return Enumerable.Range(lowerBound, upperBound - lowerBound + 1);
    }

The code may be a bit faster by replacing the Enumerable.Range with a classic for loop, but with only 1000 numbers to enumerate, it's not a critical issue.

share|improve this answer
    
NumberOfDigits = 3 was correct, was it not? –  Paul Martel Jan 6 '12 at 13:34
    
@PaulMartel: yeah, I've tested with other values for N, and I've forgotten to set 3 again ;) –  Steve B Jan 6 '12 at 13:37
    
Nice and simple, I like it so much more than mine! 2 comments: dig being a word, I would prefer to name the var digit; I would also rather call n remainder and use it destructively, than copying the argument into it and using the argument destructively. –  ANeves Jan 6 '12 at 13:39
1  
@Aneves: I've copy paste the code from the suggested link from Mike Nakis. I've kept the variable name + adjustements for my code (the original code was in C++ I think) –  Steve B Jan 6 '12 at 13:43
    
Good idea keeping the isPalindrome test numeric, but it could still borrow from @Mike and get by with checking half the digits. –  Paul Martel Jan 6 '12 at 13:46
add comment

I second @MikeNakis.
Additionally:

If you only want the largest, why not check from the top down? Just mind the pitfall that the first found might not be the largest.
(583 x 995 = 580085; 517 x 995 = 514415; 913 x 993 = 906609; 121 x 991 = 119911)

private int? GetLargestNumericPalindrome(int minBound, int maxBound) {
    int largestFound = minBound - 1;
    for (int i = maxBound; i >= minBound; i--) {
        // "taking advantage of the fact that A*B = B*A"
        // So we don't need to check N*maxBound since we already checked maxBound*N...
        // So only checking from N*N downwards.
        for (int k = i; k >= minBound; k--) {
            int num = i*k;
            if (num <= largestFound) {
                break; // Any other values of k for this i will also be lower.
            }
            if (IsPalindrome(num)) {
                largestFound = num;
                if(k > minBound) {
                    // Black magic. Prove with logic and THEN uncomment this next line.
                    //minBound = k;
                }
                break;
            }
        }
    }
    if(largestFound < minBound) {
        return null;
    }
    return largestFound;
}

private bool IsPalindrome(int num) {
    // Use OP's implementation.
    // Consider @MikeNakis's comments.
    throw new NotImplementedException();
}
share|improve this answer
    
+1 LOL ! should have thought of that! 8-) (But you are still not taking advantage of the fact that A*B = B*A. You can improve it even further!) –  Mike Nakis Jan 6 '12 at 10:07
    
do this actually works, or is this just a coincidence? What about numbers with any N digits, and not N = 3 ? –  Steve B Jan 6 '12 at 10:09
    
Is that a trick question? [First question.] Logic dictates it works the same: but instead of working our way up and replacing our found value for "max", we work our way down and look for the first find; that value will automatically be the max, since if we continue we will only find smaller values. Does this explain your question? If it does not, you will have to make a more concrete question. [Second question.] As for numbers with digits!=3, that is not part of the original question/issue/problem; but it could be addressed by calculating the 999 and the 100, instead of using static values. –  ANeves Jan 6 '12 at 10:46
    
@ANeves when you are replying to someone, use '@' + their nickname, so that they receive notification. Also, a separate answer to each person is better, so as to avoid confusion. Because I am now confused by your above comment. –  Mike Nakis Jan 6 '12 at 12:01
    
@MikeNakis woops! and thanks. –  ANeves Jan 6 '12 at 12:10
show 9 more comments

Building on @Neves, but filtering with multiples of 10 and leveraging the inner for loop to reduce multiplications:

private int? GetLargestNumericPalindrome(int minBound, int maxBound) {
    int largestFound = 0;
    for (int i = maxBound; i >= minBound; i--) {
        // Normally, numbers don't have leading zeros, 
        // so palindromes don't have trailing zeros.
        // Testing this up front seems worthwhile (almost a 10% improvement?).
        if (i % 10 == 0) {
            continue;
        }
        int smallestWorthChecking = i * minBound;
        if (smallestWorthChecking <= largestFound) {
            smallestWorthChecking = largestFound + 1;
        }
        // "taking advantage of the fact that A*B = B*A"
        // So we don't need to check N*maxBound since we already checked maxBound*N...
        // So only checking from N*N downwards.
        for (int num = i*i; num >= smallestWorthChecking; num -= i) {
            // ... palindromes don't have trailing zeros.
            if (num % 10 != 0 && IsPalindrome(num)) {
                largestFound = num;
                break;
            }
        }
    }
    if(largestFound == 0) {
        return null;
    }
    return largestFound;
}

synthesizing ideas from @MikeNakis -- no need to reverse/match the whole, and @SteveB -- keeping it numeric

static bool IsPalindrome(long testNumber)
{
    var reversed = 0L;
    var forward = testNumber
    while (reversed < forward)
    {
        var digit = forward % 10;
        reversed = reversed * 10 + digit;
        forward = forward / 10;
    }
    // Here, reversed can have at most 1 more digit than forward
    // Test for even palindromes, forward and reversed identical,
    // or odd palindromes, reversed adds 1 extra digit
    return forward == reversed || forward == reversed / 10;
}
share|improve this answer
add comment

In addition to Mike Nakis's great suggestions, I think the IsPalindrome function can be made more simple and readable like this:

static bool IsPalindrome(long testNumber)
{
    var stringRepresentation = testNumber.ToString("d");

    return stringRepresentation == Reverse(stringRepresentation);
}

private static string Reverse(string stringRepresentation)
{
    var charArray = stringRepresentation.ToCharArray();
    Array.Reverse(charArray);

    return new string(charArray);
}

This also reduced the total execution time on my machine from 416ms to 246ms.

share|improve this answer
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.