Skip to content

Commit

Permalink
Replies to comments
Browse files Browse the repository at this point in the history
  • Loading branch information
Dim131 committed May 6, 2024
1 parent 62d3096 commit 54e5294
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 31 deletions.
1 change: 1 addition & 0 deletions _data/contests/36-PDP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ luckyagain:
codes_in_git: true
solution_tags: ["map", "bst", "counting", "decimal numbers", "number bases"]
on_judge: false
points: 30
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ typedef long long ll;

bool is_lucky(const std::vector<int>& digitsA, const std::vector<int>& digitsB) {
int total_length = (digitsA.size() + digitsB.size());
if (total_length % 2 == 1) return false;
if (total_length % 2 == 1) return false; // Αν έχει μονό πλήθος ψηφίων, τότε δεν είναι τυχερός.

int cur_sum[2];
cur_sum[1] = 0; // Το άθροισμα των ψηφίων του πρώτου μισού.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
#include <cstdio>
#include <unordered_map>
#include <vector>

typedef long long ll;

ll counts[10 /* πλήθος ψηφίων */][82 /* άθροισμα ψηφίων */]; // Πλήθος αριθμών

int main() {
FILE *fi = fopen("luckyagain.in", "r");
long N;
fscanf(fi, "%ld", &N);

std::vector<std::vector<int>> digits(N);
std::vector<int> digit_sum(N, 0);
std::unordered_map<int /* άθροισμα ψηφίων */, long long /* πλήθος αριθμών */>
counts[10 /* πλήθος ψηφίων */];

for (int i = 0; i < N; ++i) {
long temp;
Expand All @@ -32,8 +31,8 @@ int main() {
fclose(fi);

ll total_count = 0;
// Οι αριθμοί με το ίδιο πλήθος ψηφίων θα μετρηθούν δύο φορές,
// επομένως κρατάμε το πλήθος αυτών ώστε να το αφαιρέσουμε από το σύνολο.
// Οι αριθμοί με το ίδιο πλήθος ψηφίων θα μετρηθούν δύο φορές. Επομένως κρατάμε
// το πλήθος αυτών ώστε να το αφαιρέσουμε το μισό από το σύνολο.
ll same_digit_count = 0;
for (int j = 0; j < N; ++j) {
const int sum_of_digits = digit_sum[j];
Expand All @@ -45,7 +44,9 @@ int main() {
for (int i = 0; 2 * i < len; ++i) {
ll cur_count = counts[len - 2 * i][sum_of_digits - 2 * suffix_sum];
total_count += cur_count;
if (i == 0) same_digit_count += cur_count - 1;
if (i == 0) { // Το μέσο είναι στο τέλος του αριθμού.
same_digit_count += cur_count - 1; // -1, για να μην μετρήσουμε τον εαυτό του.
}
suffix_sum += digits[j][i];
}

Expand All @@ -55,7 +56,9 @@ int main() {
for (int i = 0; 2 * i < len; ++i) {
ll cur_count = counts[len - 2 * i][sum_of_digits - 2 * prefix_sum];
total_count += cur_count;
if (i == 0) same_digit_count += cur_count - 1;
if (i == 0) { // Το μέσο είναι στην αρχή του αριθμού.
same_digit_count += cur_count - 1;
}
prefix_sum += digits[j][len - i - 1];
}

Expand All @@ -69,4 +72,4 @@ int main() {
fclose(fo);

return 0;
}
}
38 changes: 25 additions & 13 deletions contests/_36-PDP/c-luckyagain-solution.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,63 @@ codename: luckyagain

## Επεξήγηση εκφώνησης

Ένας αριθμός είναι τυχερός αν έχει ζυγό αριθμό ψηφίων και τα πρώτα μισά του ψηφία έχουν το ίδιο άθροισμα με τα τελευαία μισά.
Ένας αριθμός είναι *τυχερός* αν έχει ζυγό αριθμό ψηφίων και τα πρώτα μισά του ψηφία έχουν το ίδιο άθροισμα με τα τελευαία μισά.

Μας δίνεται μία λίστα από $$N$$ αριθμούς και πρέπει να βρούμε πόσα ζευγάρια από αριθμούς μπορούμε να ενώσουμε ώστε να πάρουμε έναν τυχερό αριθμό.

## Υπολογίζοντας τα ψηφία ενός αριθμού

Στις δύο παρακάτω λύσεις θα χρησιμοποιήσουμε τον παρακάτω αλγόριθμο για την εύρεση των δεκαδικών ψηφίων ενός αριθμού. Ο αλγόριθμος βρίσκει διαδοχικά το τελευταίο ψηφίο του αριθμού (δηλαδή το υπόλοιπο του αριθμού με το $$10$$), αφαιρεί το τελευταίο ψηφίο (δηλαδή διαιρεί (ακέραια) τον αριθμό με το $$10$$) και συνεχίζει με τον υπόλοιπο αριθμό μέχρι να γίνει $$0$$. Για παράδειγμα, αν ξεκινήσουμε με τον αριθμο $$x = 257$$:
Στις δύο παρακάτω λύσεις θα χρησιμοποιήσουμε τον εξής αλγόριθμο για την εύρεση των ψηφίων ενός αριθμού στην δεκαδική του αναπαράσταση. Ο αλγόριθμος βρίσκει διαδοχικά το τελευταίο ψηφίο του αριθμού (δηλαδή το υπόλοιπο του αριθμού με το $$10$$), αφαιρεί το τελευταίο ψηφίο (δηλαδή διαιρεί (ακέραια) τον αριθμό με το $$10$$) και συνεχίζει με τον υπόλοιπο αριθμό μέχρι να γίνει $$0$$. Για παράδειγμα, αν ξεκινήσουμε με τον αριθμο $$x = 257$$:
- Στην πρώτη επανάληψη, λαμβάνουμε $$257 \bmod 10 = 7$$ (το πρώτο ψηφίο) και έπειτα θέτουμε $$x = 257 / 10 = 25$$.
- Στην δεύτερη επανάληψη, λαμβάνουμε $$25 \bmod 10 = 5$$ (το δεύτερο ψηφίο) και έπειτα θέτουμε $$x = 25 / 10 = 2$$.
- Στην δεύτερη επανάληψη, λαμβάνουμε $$25 \bmod 10 = 2$$ (το τρίτο ψηφίο) και έπειτα θέτουμε $$x = 2 / 10 = 0$$.
- Στην τρίτη επανάληψη, λαμβάνουμε $$25 \bmod 10 = 2$$ (το τρίτο ψηφίο) και έπειτα θέτουμε $$x = 2 / 10 = 0$$.
- Ο αριθμός έγινε μηδέν επομένως δεν έχει άλλα ψηφία.

Ο παρακάτω κώδικας υλοποιεί αυτόν τον αλγόριθμο και χρειάζεται χρόνο γραμμικό στο πλήθος των ψηφίων του αριθμού:
Ο παρακάτω κώδικας υλοποιεί αυτόν τον αλγόριθμο και χρειάζεται χρόνο γραμμικό στο πλήθος των ψηφίων του αριθμού:[^1]

[^1]: Στην C++ υπάρχουν επίσης οι συναρτήσεις `itoa` και `sprintf` στην standard βιβλιοθήκη, που κάνουν αυτούς τους υπολογισμούς (δείτε [εδώ](https://cplusplus.com/reference/cstdlib/itoa/) και [εδώ](https://cplusplus.com/reference/cstdio/sprintf/)).

{% include code.md solution_name='luckyagain_brute_force.cc' start=38 end=41 %}

## Εξαντλητική λύση

Χρησιμοποιώντας τον παραπάνω αλγόριθμο για την εύρεση των ψηφίων ενός αριθμού, μπορούμε να ελέγξουμε αν ένωση δύο αριθμών είναι τυχερός αριθμός "ενώνοντας" τα ψηφία τους, αθροίζοντας τα πρώτα και τα τελευταία μισά, και τέλος ελέγχοντας αν το άθροισμά τους είναι ίσο. Ο αλγόριθμος που το κάνει αυτό είναι ο εξής (και είναι γραμμικός στο πλήθος των ψηφίων):
Χρησιμοποιώντας τον παραπάνω αλγόριθμο για την εύρεση των ψηφίων ενός αριθμού, μπορούμε να ελέγξουμε αν η ένωση δύο αριθμών είναι τυχερός αριθμός "ενώνοντας" τα ψηφία τους, αθροίζοντας τα πρώτα και τα τελευταία μισά, και τέλος ελέγχοντας αν το άθροισμά τους είναι ίσο. Ο αλγόριθμος που το κάνει αυτό είναι ο εξής (και είναι γραμμικός στο πλήθος των ψηφίων):

{% include code.md solution_name='luckyagain_brute_force.cc' start=7 end=24 %}

Τέλος αρκεί να ελέγξουμε κάθε δυνατό ζεύγος από τους δοσμένους αριθμούς και να μετρήσουμε το πλήθος αυτών που σχηματίζουν τυχερούς αριθμούς.
Τέλος, αρκεί να ελέγξουμε κάθε δυνατό ζεύγος από τους δοσμένους αριθμούς και να μετρήσουμε το πλήθος αυτών που σχηματίζουν τυχερούς αριθμούς.

{% include code.md solution_name='luckyagain_brute_force.cc' start=48 end=55 %}

Συνολικά κάνουμε $$\mathcal{O}(N^2)$$ ελέγχους και κάθε ένας από αυτούς χρειάζεται το πολύ σταθερό αριθμό πράξεων (γιατί τα ψηφία κάθε αριθμού είναι το πολύ $$9$$). Μπορείτε να βρείτε ολόκληρο τον κώδικα [εδώ]({% include link_to_source.md solution_name='luckyagain_brute_force.cc' %})
Συνολικά ελέγχουμε $$Ν \cdot (N - 1)$$ ζευγάρια και κάθε έλεγχος χρειάζεται το πολύ σταθερό αριθμό πράξεων (γιατί τα ψηφία κάθε αριθμού είναι το πολύ $$9$$). Μπορείτε να βρείτε ολόκληρο τον κώδικα [εδώ]({% include link_to_source.md solution_name='luckyagain_brute_force.cc' %})

## Βέλτιστη λύση

Ας υποθέσουμε ότι για κάποιον αριθμό $$x$$ θέλουμε να μετρήσουμε όλους τους αριθμούς $$y$$ για τους οποίους ισχύει ότι η ένωση τους $$xy$$ είναι τυχερός αριθμός. Το ποια ψηφία του $$x$$ συνεισφέρουν στο πρώτο ή στο δεύτερο μισό του ενωμένου αριθμού, εξαρτάται από το που βρίσκεται το μέσο τους. Ας θεωρήσουμε ότι το μέσο τους βρίσκεται αμέσως μετά το $$i$$-οστό ψηφίο του $$x$$. Τότε, αν $$\ell_x$$ είναι το πλήθος των ψηφίων του $$x$$, ψάχνουμε για οποιουσδήποτε αριθμούς $$y$$ με $$\ell_y = 2i - \ell_x$$ ψηφία (καθώς πρέπει $$i = (\ell_x - i) + \ell_y$$ για να είναι $$i$$ το μέσο) και άθροισμα ψηφίων $$s_y$$ τέτοιο ώστε
Ας υποθέσουμε ότι για κάποιον αριθμό $$x$$ θέλουμε να μετρήσουμε όλους τους αριθμούς $$y$$ για τους οποίους ισχύει ότι η ένωση τους $$xy$$ είναι τυχερός αριθμός. Θέλουμε να το κάνουμε αυτό χωρίς να πρέπει να δοκιμάσουμε όλους τους δυνατούς αριθμούς $$y$$.

$$x_1 + \ldots x_i = x_{i+1} + \ldots + x_{\ell_x} + s_y \Leftrightarrow s_y = (x_1 + \ldots x_i) - (x_{i+1} + \ldots + x_{\ell_x}).$$
Το ποια ψηφία του $$x$$ συνεισφέρουν στο πρώτο και ποια στο τελευταίο μισό του αριθμού $$xy$$, εξαρτάται από το που βρίσκεται το μέσο τους. Ας θεωρήσουμε ότι το μέσο τους βρίσκεται αμέσως μετά το $$i$$-οστό ψηφίο του $$x$$, όπως στο ακόλουθο σχήμα (όπου $$\ell_x$$ και $$\ell_y$$ είναι το πλήθος των ψηφίων του $$x$$ και $$y$$ αντίστοιχα).

<center>
<img width=450px src="/assets/36-c-luckyagain.svg"/>
</center>

Προϋπολογίζουμε τον πίνακα $$\texttt{count}[\ell][s]$$, που κρατάει το πλήθος των αριθμών με $$\ell$$ ψηφία και άθροισμα ψηφίων $$s$$. Έπειτα, για κάθε αριθμό $$x$$ διατρέχουμε όλες τις θέσεις $$i$$ των ψηφίων του και αθροίζουμε το πλήθος των αριθμών
$$\texttt{count}[2i - \ell_x][(x_1 + \ldots x_i) - (x_{i+1} + \ldots + x_{\ell_x})]$$.
Τότε, ψάχνουμε για όλους τους αριθμούς $$y$$ με $$\ell_y = 2i - \ell_x$$ ψηφία (καθώς πρέπει $$i = (\ell_x - i) + \ell_y$$ για να είναι $$i$$ το μέσο) και άθροισμα ψηφίων $$s_y$$ τέτοιο ώστε

$$
x_1 + \ldots + x_i = x_{i+1} + \ldots + x_{\ell_x} + s_y \\
\Leftrightarrow \\
s_y = (x_1 + \ldots x_i) - (x_{i+1} + \ldots + x_{\ell_x}).
$$

Για να βρούμε το πλήθος αυτών των αριθμών γρήγορα, προϋπολογίζουμε τον πίνακα $$\texttt{count}[\ell][s]$$, που κρατάει το πλήθος των αριθμών με $$\ell$$ ψηφία και άθροισμα ψηφίων $$s$$. Έπειτα, για κάθε αριθμό $$x$$ διατρέχουμε όλες τις θέσεις $$i$$ των ψηφίων του και αθροίζουμε το πλήθος των αριθμών
$$\texttt{count}[2i - \ell_x][(x_1 + \ldots + x_i) - (x_{i+1} + \ldots + x_{\ell_x})]$$.

Μία αντίστοιχη συμμετρική συνθήκη προκύπτει όταν το $$x$$ έρχεται μετά το $$y$$ (και το μέσο βρίσκεται πάλι στο $$x$$).

Μία αντίστοιχη συμμετρική συνθήκη προκύπτει όταν το $$x$$ έρχεται μετά το $$y$$ (και το μέσο βρίσκεται πάλι στο $$x$$). Χρειάζεται λίγο προσοχή με τα ζεύγη αριθμών με το ίδιο πλήθος ψηφίων, ώστε να μην τα διπλομετρήσουμε. Στον παρακάτω κώδικα η μεταβλητή $$\texttt{same\_digit\_count}$$ φροντίζει για αυτό.
Χρειάζεται **προσοχή** με τα ζεύγη αριθμών με το *ίδιο* πλήθος ψηφίων, καθώς αυτά θα τα μετρήσουμε δύο φορές, μία όταν ψάχνουμε για τυχερούς αριθμούς της μορφής $$x~\cdot$$ και μία όταν ψάχνουμε για αριθμούς της μορφής $$\cdot~y$$. Στον παρακάτω κώδικα η μεταβλητή $$\texttt{same\_digit\_count}$$ μετράει πόσες τέτοιες περιπτώσεις υπάρχουν και αφαιρεί τις μισές από αυτές στο τέλος. Πρέπει επίσης να προσέξουμε να μην μετρήσουμε τα ζευγάρια της μορφής $$xx$$.


Κάθε πρόσβαση στον πίνακα κατακερματισμού χρειάζεται $$\mathcal{O}(1)$$ χρόνο$$\mathcal{O}(\log N)$$ αν χρησιμοποιήσουμε map) και επειδή κάθε αριθμός έχει το πολύ $$9$$ ψηφία, συνολικά ο αλγόριθμος χρειάζεται $$\mathcal{O}(N)$$ χρόνο. Ο κώδικας δίνεται παρακάτω.
Κάθε πρόσβαση στον πίνακα χρειάζεται $$\mathcal{O}(1)$$ χρόνο και επειδή κάθε αριθμός έχει το πολύ $$9$$ ψηφία, συνολικά ο αλγόριθμος χρειάζεται $$\mathcal{O}(N)$$ χρόνο. Ο κώδικας δίνεται παρακάτω.

{% include code.md solution_name='luckyagain_efficient.cc' %}

Expand Down
22 changes: 13 additions & 9 deletions contests/_36-PDP/c-luckyagain-statement.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,32 @@ codename: luckyagain
## Αρχεία εξόδου:
Το αρχείο εξόδου με όνομα **luckyagain.out** είναι αρχείο κειμένου που περιέχει μία μόνο γραμμή με έναν ακέραιο αριθμό: το πλήθος των διαφορετικών τρόπων που μπορούν να σχηματιστούν τυχεροί αριθμοί κολλώντας δύο χαρτάκια.

##
## Παραδείγματα

**1<sup>o</sup>**

| **luckyagain.in** | **luckyagain.out** |
| :--- | :--- |
| 7<br>75038 92 1 728 83 5 423 | 6 |

*Εξήγηση:* Μπορούν να σχηματιστούν τυχεροί αριθμοί κολλώντας δύο χαρτάκια με 6 διαφορετικούς τρόπους.
- κολλώντας το 1 και το 423 προκύπτει ο τυχερός αριθμός 1423,
- κολλώντας το 75038 και το 1 προκύπτει ο τυχερός αριθμός 750381,
- κολλώντας το 92 και το 83 προκύπτει ο τυχερός αριθμός 9283,
- κολλώντας το 728 και το 1 προκύπτει ο τυχερός αριθμός 7281,
- κολλώντας το 83 και το 92 προκύπτει ο τυχερός αριθμός 8392, και
- κολλώντας το 423 και το 75038 προκύπτει ο τυχερός αριθμός 42375038.
*Εξήγηση:* Μπορούν να σχηματιστούν τυχεροί αριθμοί κολλώντας δύο χαρτάκια με $$6$$ διαφορετικούς τρόπους.
- κολλώντας το $$1$$ και το $$423$$ προκύπτει ο τυχερός αριθμός $$1423$$,
- κολλώντας το $$75038$$ και το $$1$$ προκύπτει ο τυχερός αριθμός $$750381$$,
- κολλώντας το $$92$$ και το $$83$$ προκύπτει ο τυχερός αριθμός $$9283$$,
- κολλώντας το $$728$$ και το $$1$$ προκύπτει ο τυχερός αριθμός $$7281$$,
- κολλώντας το $$83$$ και το $$92$$ προκύπτει ο τυχερός αριθμός $$8392$$, και
- κολλώντας το $$423$$ και το $$75038$$ προκύπτει ο τυχερός αριθμός $$42375038$$.

**2<sup>o</sup>**

| **luckyagain.in** | **luckyagain.out** |
| :--- | :--- |
| 6<br>265 10387 392 981 6986 74 | 0 |

*Εξήγηση:* Δεν μπορεί να σχηματιστεί κανένας τυχερός αριθμός.

**3<sup>o</sup>**

| **luckyagain.in** | **luckyagain.out** |
| :--- | :--- |
| 5<br>17 62 35 44 80 | 20 |
Expand All @@ -54,7 +58,7 @@ codename: luckyagain
## Περιορισμοί:
- $$1 \leq N \leq 1.000.000$$,
- Οι αριθμοί θα είναι το πολύ εννιαψήφιοι και το πρώτο ψηφίο θα είναι πάντα μη μηδενικό.
- Για περιπτώσεις ελέγχου συνολικής αξίας 30%, θα είναι $$N \leq 1000$$.
- Για περιπτώσεις ελέγχου συνολικής αξίας 30%, θα είναι $$N \leq 1.000$$.
- Για περιπτώσεις ελέγχου συνολικής αξίας 50%, οι αριθμοί θα είναι πενταψήφιοι ή εξαψήφιοι.

**Προσοχή!** Η απάντηση μπορεί να υπερβαίνει το $$2^{32}$$. Επίσης, φροντίστε να διαβάζετε την είσοδο και να εκτυπώνετε την έξοδο αποδοτικά, ειδικά αν προγραμματίζετε σε C++ ή Java.
Expand Down

0 comments on commit 54e5294

Please sign in to comment.