CS1010 Notes
  • Welcome
  • Lec/Tut/Lab/Exes
    • Lecture
      • Lec 01 - Computational Problem Solving
      • Lec 02 - Functions and Types
      • Lec 03 - Basic C Programming
      • Lec 04 - Conditionals
      • Lec 05 - Loops
      • Lec 06 - Call Stacks, Arrays
        • Diagnostic Quiz
      • Lec 07 - Pointers, Memory management
        • Diagnostic Quiz
      • Lec 08 - Multi-d Array, Efficiency
        • Diagnostic Quiz
      • Lec 09 - Searching and Sorting
        • Diagnostic Quiz
      • Lec 10 - More Recursion
        • Diagnostic Quiz
      • Lec 11 - Strcut & Standard I/O
        • Diagnostic Quiz
      • Lec 12 - Recap
    • Tutorial
      • Tut 01 - Computational Problem-Solving
      • Tut 02 - Functions and Conditionals
      • Tut 03 - More on Conditionals
      • Tut 04 - Loops
      • Tut 08 - Searching and Sorting
    • Lab
      • Lab 01 - Unix/Vim Setup
      • Lab 02 - Debugging
      • Lab 03 - Assert
      • Lab 04 - Test Cases
      • Lab 05 - Arrays
      • Lab 06 - Memory Errors
      • Lab 07 - Compiling with Clang
      • Lab 08 - C Preprocessor
      • Lab 09 - Backtracking
      • Lab 10 - Struct and Wrap up
    • Exercises
      • Exercise 3 - Fixed-Length Arrays
      • Exercise 4 - Dynamic Arrays and Strings
      • Exercise 6 - Searching and Sorting
      • Exercise 7 - More Recursion
      • Exercise 8 - Struct
  • Past Year Exam
    • Midterm PE
      • PE1 (AY18/19)
      • PE1 (AY20/21)
      • PE1 (AY21/22)
      • PE0 (AY22/23)
      • PE0 (AY23/24)
    • Midterm Paper
      • Midterm (AY18/19)
      • Midterm (AY20/21)
      • Midterm (AY21/22)
      • Midterm (AY22/23)
    • PE1 Review
      • PE1 (AY23/24)
    • PE2 Review
      • PE2 (AY18/19)
      • PE2 (AY20/21)
      • PE2 (AY21/22)
      • PE2 (AY22/23)
      • PE2 (AY23/24)
    • Final Paper
      • Final (AY18/19)
      • Final (AY20/21)
      • Final (AY21/22)
      • Final (AY22/23)
      • Final (AY23/24)
  • Current Year Exam
    • PE0 (AY24/25)
    • PE1 (AY24/25)
    • PE2 (AY24/25)
    • Final (AY24/25)
  • Toolbox
    • Vim & Unix
    • GDB
  • After CS1010
Powered by GitBook
On this page
  • Problems
  • 1. Error*
  • 2. Twilight
  • 3. Reverse*
  • 4. Unique*
  • 5. Look-N-Say
  • Tips
Edit on GitHub
  1. Past Year Exam
  2. Midterm PE

PE1 (AY20/21)

Problems

1. Error*

Ignore kkk most significant digits in num nnn

Basic idea is to use num nnn to modular 10k10^k10k. And here is the code for implementation:

long ignore_first_k_digit(long n, long k)
{
    long power = 1;
    for (int i = 0; i < k; i += 1) {
        power *= 10;
    }
    return n % power;
}

For example, if we input ignore_last_k_digit(1234, 2), the power will be raised to 100 and we will get 1234 % 100 = 34.

Ignore the kkk least significant digits in num

The basic idea here is to use integer division, so we use n/10k⋅10kn / 10^k \cdot 10^kn/10k⋅10k.

long ignore_last_k_digit(long n, long k)
{
    long power = 1;
    for (int i = 0; i < k; i += 1) {
        power *= 10;
    }
    return n / power * power;
}

For example, if we input ignore_first_k_digit(1234, 2), then power will be raised to 100. So, we will return 1234 / 100 * 100, which is 12 * 100 = 1200.

2. Twilight

This is a "give-away" question.

3. Reverse*

Naive idea

The basic and and naive idea for this question is:

  1. take the last digit of nnn

  2. reverse the rest of the digits of nnn

  3. put the last digit of nnn "in front"

And the trickest part lies in how to put the last digit of nnn in front? The basic idea is that if we know the digit position/ the number of digits (Let's say it's kkk), we can quickly put the digit in front by timing it with 10k10^k10k. So now, we need to write another recursive function that can help us calculate the amount of power we should time it with the last digit of nnn.

long power(long num)
{
    if (num < 10)
    {
        return 1;
    }
    return 10 * power(num / 10);
}

This is known as the recursive way to calculate the weight we need to put a digit at the front of the num nnn in decimal.

A more elegant solution

Assume that we already know how to reverse a shorter number given that part of the number is already reversed (Suppose we divide 123456 into 1234 and 65):

  1. We can get a shorter number by removing the last digit (1234 -> 123)

  2. We can extend the partial solution by appending the last digit (65 -> 654)

  3. We just return the value returned by the recursive call!

Code Implementation:

long reverse(long remaining, long reversed)
{
    if (remaining == 0) {
        return reversed;
    }
    return reverse(remaining / 10, reversed * 10 + (remaining % 10));
}

The base case means that we there is no digit remaining, we have already reversed all the digit! And the elegant idea here is we shift the reversed digit right by one "bit", that saves us from the trouble of calculating the power! Awesome!

4. Unique*

A pretty awesome question to thinking about!!!

Setup (Preparation)

First thing first, we can use a while loop inside a for loop to find the how many times a factor has repeated. The implementation is below:

long curr = n;
for (long factor = 2; factor < n; factor += 1) {
    if (is_prime(factor)) {
        long count = 0;
        while (curr % factor == 0) {
            curr = curr / factor;
            count += 1;
        }
        if (count != 0) {
            print(factor, count);
        }
    }
}

Rememebr to define another variable curr to store the num n. And remember to initialize the count to 0 every time you start with a different factor.

First Optimization

Then let's think about how to improve the efficieny, first we can check the factor tilll sqrt(n) instead of n.

Proof (Why can we change the loop condition to sqrt(n)?)

Suppose we have checked (eliminated) all the factors that are smaller than or equal to sqrt(n), now let's say there is a factor (let's define it to be kkk) of nnn that is bigger than sqrt(n). Since we have already elminated all the factors smaller than or equal to sqrt(n), that means kkk cannot have the factors smaller than or eqaul to sqrt(k) (we already know what k<nk<nk<n). So, kkk must be a prime and the last factor of our number nnn.f(x)=x∗e2piiξxf(x) = x * e^{2 pi i \xi x}f(x)=x∗e2piiξx

Now, we can change our code to below

long curr = n;
for (long factor = 2; factor <= sqrt(n); factor += 1) {
    if (is_prime(factor)) {
        long count = 0;
        while (curr % factor == 0) {
            curr = curr / factor;
            count += 1;
        }
        if (count != 0) {
            print(factor, count);
        }
    }
}
if (curr != 1)
{
    print(curr, 1);
}

Remember the part outside the loop, see Proof (Why can we change the loop condition to sqrt(n)?).

Second Optimization

Next let's think about it deeper. Since we are dealing with curr every time in the loop, so actually we are finding the factors of curr instead of n. So, we can change the condition of our loop to ...sqrt(curr).... Below is the further optimized version:

long curr = n;
for (long factor = 2; factor <= sqrt(curr); factor += 1) { // <-- Changed here
    if (is_prime(factor)) {
        long count = 0;
        while (curr % factor == 0) {
            curr = curr / factor;
            count += 1;
        }
        if (count != 0) {
            print(factor, count);
        }
    }
}
if (curr != 1)
{
    print(curr, 1);
}

Third Optimization

Can we do it even better? Since now we still cannot pass the test case 5, which is when the input is 2622^{62}262. Actually, we can stop our loop is the curr == 1 and when our curr is a prime. So, we can optimize our code further to below:

Proof (Why we can stop when curr == 1 or curr is a prime?

The first case is easy to understand. No more explanation is needed. The second case is actually easy to understand also. Think about it, curr is the number remaining for you to check whether it will have factors or not. So, it your curr is already a prime, it confirms cannot have any factors. So, you can stop!

long curr = n;
if (is_prime(curr))
{
    print(curr, 1);
    return;
}
for (long factor = 2; factor <= sqrt(curr) && curr != 1; factor += 1) { // <-- Changed here
    if (is_prime(factor)) {
        long count = 0;
        while (curr % factor == 0) {
            curr = curr / factor;
            count += 1;
        }
        if (count != 0) {
            print(factor, count);
        }
        if (is_prime(curr) && curr != 1)
        {
            print(curr, 1);
            return ;
        }
    }
}

Fourth Optimization

Before we check whether the factor is a prime or not, we can check whether the factor is indeed a factor or not. If not, then we don't need to check whether it's prime or not. This will also improve the efficiency. (This is the short-circuiting we have learend in the lecture!)

long curr = n;
if (is_prime(curr))
{
    print(curr, 1);
    return;
}
for (long factor = 2; factor <= sqrt(curr) && curr != 1; factor += 1) { // <-- Changed here
    if ((curr % factor == 0) && is_prime(factor)) {
        long count = 0;
        while (curr % factor == 0) {
            curr = curr / factor;
            count += 1;
        }
        if (count != 0) {
            print(factor, count);
        }
        if (is_prime(curr) && curr != 1)
        {
            print(curr, 1);
            return ;
        }
    }
}

Fifth Optimization

But, see how, the most interesting part now comes! We can do this even without using is_prime()!!! Think about it – we are starting with the smallest factor, and we divide the input with the factor as many times as we can. So at the end of the for loop, we can assert that, curr is not divisible by anything between 2 and factor. Consequently, the next factor for curr has to be prime! Similar to this Proof (Why can we change the loop condition to sqrt(n)?) right? So, we've freed is_prime() and the following code works just fine!

long curr = n;
for (long factor = 2; factor <= sqrt(n) && curr != 1; factor += 1) {
    long count = 0;
    while (curr % factor == 0) {
        curr = curr / factor;
        count += 1;
    }
    if (count != 0) {
        print(factor, count);
    }
}
if (curr != 1) {
    print(curr, 1);
}

Note that in the loop condition, we set the terminating factor to be sqrt(n).

Proof (Why is the last factor the only one factor remaining?)

It's quite easy if you think about it. Since we now exit the loop, which means the last factor must be bigger than sqrt(n), so let's say if you have two such factors remaining, you times them up and your result will definitely be greater than n right? That's impossible! Proof.

Sixth Optimization

Continue thinking about it? Can we optimize further? The answer is yes!!! Using the idea of narrowing down the loop condition from n to curr in the Second Optimization, we can apply this idea here too.

Proof (Why we can change n to curr here?)

First, let's clarify our goal. Remember that the reason we can update n to curr is because that we only need to find the factors of curr. So, after every iteration, we have already eliminated all the factors that are smaller than sqrt(curr). This ensures that when we reach the termination of the loop, the remaing curr must be the last remaining prime one.

So, now our most succinct code should be:

long curr = n;
for (long factor = 2; factor <= sqrt(curr) && curr != 1; factor += 1) {
    long count = 0;
    while (curr % factor == 0) {
        curr = curr / factor;
        count += 1;
    }
    if (count != 0) {
        print(factor, count);
    }
}
if (curr != 1) {
    print(curr, 1);
}

5. Look-N-Say

The first part if "look", which is implemented as below:

long count = 0;
long last_digit = n % 10;
do {
    n = n / 10;
    count += 1;
} while (last_digit == n % 10);

The second part is "say", which is implemented as below:

say = say + (count * 10 + last_digit) * factor;
factor *= 100; // factor is used to shift right

Now, combining these two parts, we can get our "main" function:

long look_and_say(long n)
{
    long say = 0;
    long factor = 1;
    while (n != 0) {
        // look
        long count = 0;
        long last_digit = n % 10;
        do {
            n = n / 10;
            count += 1;
        } while (last_digit == n % 10);
        // say
        say = say + (count * 10 + last_digit) * factor;
        factor *= 100;
    }
    return say;
}

The function above perform one step of look and say. To find the kkk-th look-n-say number, we simply do this:

for (long i = 1; i < k; i += 1) {
    n = look_and_say(n % 100000000);
}

% 100000000 here is to get the last 8 digits only for generating the next number.

Tips

  1. The use of return statement in the loop is allowed in the loop. It will cause the current stack frame to the function to be destroyed.

PreviousPE1 (AY18/19)NextPE1 (AY21/22)

Last updated 8 months ago

As the hardest question in PE1, this question is actually not that hard. It is quite similar to the last year's PE1 about .

The most important part, as you have seen in AY18/19's PE1 , is the use of do-while loop here.

Marks won't be deducted if you use break/continue statement in the loop.

The is_prime() and will be very very useful! Include in the cheatsheet!

correctly
4. Digits*
4. Digits*
3. Goldbach*