From ef67ef876638fd9da3d8bd5c2bb932a9ea8d5cb4 Mon Sep 17 00:00:00 2001 From: dim131 Date: Fri, 3 May 2024 23:10:54 +0100 Subject: [PATCH 1/5] Update 36-PDP.yml --- _data/contests/36-PDP.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/_data/contests/36-PDP.yml b/_data/contests/36-PDP.yml index ebdae3b1..e73c3056 100755 --- a/_data/contests/36-PDP.yml +++ b/_data/contests/36-PDP.yml @@ -21,3 +21,15 @@ publib: codes_in_git: true solution_tags: ["bfs", "shortest-paths"] on_judge: false + +luckyagain: + full_name: "Τυχεροί αριθμοί" + stage: "c" + statement_pdf_url: "https://drive.google.com/file/d/1AXuG5L4SzRkTnljViElY8qWqOvNoHOTd/view" + statement_md: true + testcases_url: "" + solution: true + solution_author: "" + codes_in_git: true + solution_tags: ["map", "bst", "counting", "decimal numbers", "number bases"] + on_judge: false From 4f12a132900c1d48b33a08ce7e15b0b1e5825083 Mon Sep 17 00:00:00 2001 From: dim131 Date: Fri, 3 May 2024 23:11:17 +0100 Subject: [PATCH 2/5] Create c-luckyagain-statement.md --- contests/_36-PDP/c-luckyagain-statement.md | 64 ++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 contests/_36-PDP/c-luckyagain-statement.md diff --git a/contests/_36-PDP/c-luckyagain-statement.md b/contests/_36-PDP/c-luckyagain-statement.md new file mode 100644 index 00000000..ddf456b0 --- /dev/null +++ b/contests/_36-PDP/c-luckyagain-statement.md @@ -0,0 +1,64 @@ +--- +layout: statement +codename: luckyagain +--- + +Για τις ανάγκες αυτής της άσκησης, θα λέμε ότι ένας θετικός ακέραιος αριθμός είναι **τυχερός** αν ισχύουν τα εξής: + - έχει άρτιο πλήθος ψηφίων, και + - το άθροισμα των πρώτων μισών ψηφίων του είναι ίσο με το άθροισμα των δεύτερων μισών ψηφίων του. + +Για παράδειγμα: + - Ο αριθμός $$123$$ δεν είναι τυχερός, γιατί το πλήθος των ψηφίων του είναι $$3$$, που δεν είναι άρτιος αριθμός. + - Ο αριθμός $$153871$$ δεν είναι τυχερός, γιατί το άθροισμα των πρώτων μισών ψηφίων του είναι $$1+5+3=9$$, ενώ το άθροισμα των δεύτερων μισών ψηφίων του είναι $$8+7+1=16$$. + - Ο αριθμός $$3791$$ είναι τυχερός, γιατί το άθροισμα των πρώτων μισών ψηφίων του είναι $$3+7=10$$ και το άθροισμα των δεύτερων μισών ψηφίων του είναι $$9+1=10$$. + +Σας δίνονται $$N$$ χαρτάκια, καθένα από τα οποία έχει γραμμένο πάνω του έναν αριθμό. Με πόσους διαφορετικούς τρόπους μπορείτε να κολλήσετε δύο διαφορετικά χαρτάκια (δηλαδή να ενώσετε τα ψηφία των αριθμών που είναι γραμμένοι σε αυτά), έτσι ώστε να σχηματιστεί τυχερός αριθμός; + +## Πρόβλημα: +Να αναπτύξετε ένα πρόγραμμα σε μια από τις γλώσσες PASCAL, C, C++, Java το οποίο θα διαβάζει την τιμή του $$N$$ και τους αριθμούς που είναι γραμμένοι πάνω στα $$N$$ χαρτάκια, και θα εκτυπώνει το πλήθος των διαφορετικών τρόπων που μπορούν να σχηματιστούν τυχεροί αριθμοί κολλώντας δύο χαρτάκια. + +## Αρχεία εισόδου: +Το αρχείο εισόδου με όνομα **luckyagain.in** είναι αρχείο κειμένου που αποτελείται από δύο γραμμές. Η πρώτη γραμμή περιέχει έναν ακέραιο αριθμό $$N$$, το πλήθος των χαρτιών. Η δεύτερη γραμμή περιέχει $$N$$ ακέραιουςαριθμούς, χωρισμένους ανά δύο με ένα κενό διάστημα. + +## Αρχεία εξόδου: +Το αρχείο εξόδου με όνομα **luckyagain.out** είναι αρχείο κειμένου που περιέχει μία μόνο γραμμή με έναν ακέραιο αριθμό: το πλήθος των διαφορετικών τρόπων που μπορούν να σχηματιστούν τυχεροί αριθμοί κολλώντας δύο χαρτάκια. + +## + +**1o** + +| **luckyagain.in** | **luckyagain.out** | +| :--- | :--- | +| 7
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. + +| **luckyagain.in** | **luckyagain.out** | +| :--- | :--- | +| 6
265 10387 392 981 6986 74 | 0 | + +*Εξήγηση:* Δεν μπορεί να σχηματιστεί κανένας τυχερός αριθμός. + +| **luckyagain.in** | **luckyagain.out** | +| :--- | :--- | +| 5
17 62 35 44 80 | 20 | + +*Εξήγηση:* Κολλώντας οποιαδήποτε χαρτάκια προκύπτει πάντα τυχερός αριθμός. + +## Περιορισμοί: + - $$1 \leq N \leq 1.000.000$$, + - Οι αριθμοί θα είναι το πολύ εννιαψήφιοι και το πρώτο ψηφίο θα είναι πάντα μη μηδενικό. + - Για περιπτώσεις ελέγχου συνολικής αξίας 30%, θα είναι $$N \leq 1000$$. + - Για περιπτώσεις ελέγχου συνολικής αξίας 50%, οι αριθμοί θα είναι πενταψήφιοι ή εξαψήφιοι. + +**Προσοχή!** Η απάντηση μπορεί να υπερβαίνει το $$2^{32}$$. Επίσης, φροντίστε να διαβάζετε την είσοδο και να εκτυπώνετε την έξοδο αποδοτικά, ειδικά αν προγραμματίζετε σε C++ ή Java. + +**Μορφοποίηση:** Στην έξοδο, όλες οι γραμμές τερματίζουν με ένα χαρακτήρα newline.
+**Μέγιστος χρόνος εκτέλεσης:** 2 sec.
+**Μέγιστη διαθέσιμη μνήμη:** 128 MB. From b1e1886865c56b78ac10cf66de8d6aed67b08c94 Mon Sep 17 00:00:00 2001 From: dim131 Date: Fri, 3 May 2024 23:11:53 +0100 Subject: [PATCH 3/5] 36-luckyagain-codes --- .../source_code/code/36-PDP/luckyagain/TASK | 23 ++++++ .../luckyagain/luckyagain_brute_force.cc | 62 ++++++++++++++++ .../36-PDP/luckyagain/luckyagain_efficient.cc | 72 +++++++++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 _includes/source_code/code/36-PDP/luckyagain/TASK create mode 100644 _includes/source_code/code/36-PDP/luckyagain/luckyagain_brute_force.cc create mode 100644 _includes/source_code/code/36-PDP/luckyagain/luckyagain_efficient.cc diff --git a/_includes/source_code/code/36-PDP/luckyagain/TASK b/_includes/source_code/code/36-PDP/luckyagain/TASK new file mode 100644 index 00000000..0677f39b --- /dev/null +++ b/_includes/source_code/code/36-PDP/luckyagain/TASK @@ -0,0 +1,23 @@ +TASK( + name = "luckyagain", + test_count = 18, + files_dir = "testdata/36-PDP/luckyagain/", + input_file = "luckyagain.in", + output_file = "luckyagain.out", + time_limit = 2, + mem_limit = 128, + solutions = [ + SOLUTION( + name = "luckyagain_efficient", + source = "luckyagain_efficient.cc", + passes_all, + lang = "c++", + ), + SOLUTION( + name = "luckyagain_brute_force", + source = "luckyagain_brute_force.cc", + passes_up_to = 11, + lang = "c++", + ), + ] +) diff --git a/_includes/source_code/code/36-PDP/luckyagain/luckyagain_brute_force.cc b/_includes/source_code/code/36-PDP/luckyagain/luckyagain_brute_force.cc new file mode 100644 index 00000000..f48d1718 --- /dev/null +++ b/_includes/source_code/code/36-PDP/luckyagain/luckyagain_brute_force.cc @@ -0,0 +1,62 @@ +#include +#include +#include + +typedef long long ll; + +bool is_lucky(const std::vector& digitsA, const std::vector& digitsB) { + int total_length = (digitsA.size() + digitsB.size()); + if (total_length % 2 == 1) return false; + + int cur_sum[2]; + cur_sum[1] = 0; // Το άθροισμα των ψηφίων του πρώτου μισού. + cur_sum[0] = 0; // Το άθροισμα των ψηφίων του δεύτερου μισού. + + int midpoint = total_length / 2; + for (int i = 0; i < digitsA.size(); ++i) { + cur_sum[i < midpoint] += digitsA[i]; + } + for (int i = 0; i < digitsB.size(); ++i) { + cur_sum[i + digitsA.size() < midpoint] += digitsB[i]; + } + + return cur_sum[0] == cur_sum[1]; +} + +int main() { + FILE *fi = fopen("luckyagain.in", "r"); + long N; + fscanf(fi, "%ld", &N); + + std::vector> digits(N); + + for (int i = 0; i < N; ++i) { + long temp; + fscanf(fi, "%ld", &temp); + + // Βρίσκουμε τα ψηφία ενός αριθμού. + while (temp > 0) { + digits[i].push_back(temp % 10); + temp /= 10; + } + // Δεδομένου του υπόλοιπου κώδικα η ακόλουθη αντιστροφή δεν είναι + // αναγκαία (γιατί;) + // std::reverse(digits[i].begin(), digits[i].end()); + } + fclose(fi); + + // Μετράμε πόσα από τα δυνατά ζευγάρια φτιάχνουν έναν τυχερό αριθμό. + ll total_count = 0; + for (long i = 0; i < N; ++i) { + for (long j = 0; j < N; ++j) { + if (i == j) continue; + total_count += is_lucky(digits[i], digits[j]); + } + } + + FILE *fo = fopen("luckyagain.out", "w"); + fprintf(fo, "%lld\n", total_count); + fclose(fo); + + return 0; +} diff --git a/_includes/source_code/code/36-PDP/luckyagain/luckyagain_efficient.cc b/_includes/source_code/code/36-PDP/luckyagain/luckyagain_efficient.cc new file mode 100644 index 00000000..0e421b1f --- /dev/null +++ b/_includes/source_code/code/36-PDP/luckyagain/luckyagain_efficient.cc @@ -0,0 +1,72 @@ +#include +#include +#include + +typedef long long ll; + +int main() { + FILE *fi = fopen("luckyagain.in", "r"); + long N; + fscanf(fi, "%ld", &N); + + std::vector> digits(N); + std::vector digit_sum(N, 0); + std::unordered_map + counts[10 /* πλήθος ψηφίων */]; + + for (int i = 0; i < N; ++i) { + long temp; + fscanf(fi, "%ld", &temp); + + // Βρίσκουμε τα ψηφία ενός αριθμού και το άθροισμά τους. + while (temp > 0) { + int cur_digit = temp % 10; + digit_sum[i] += cur_digit; + digits[i].push_back(cur_digit); + temp /= 10; + } + + // (Προ)-ϋπολογισμός του πίνακα counts. + ++counts[digits[i].size()][digit_sum[i]]; + } + 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]; + const int len = digits[j].size(); + + // Βρίσκουμε το πλήθος των ζευγαριών που έχουν τον j-οστό αριθμό + // σαν πρώτο μέρος και μέσο η θέση (len - i). + int suffix_sum = 0; + 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; + suffix_sum += digits[j][i]; + } + + // Βρίσκουμε το πλήθος των ζευγαριών που έχουν τον j-οστό αριθμό + // σαν δεύτερο μέρος και μέσο η θέση i. + int prefix_sum = 0; + 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; + prefix_sum += digits[j][len - i - 1]; + } + + // Αφαιρούμε τον εαυτό του απο την μέτρηση. + total_count -= 2; + } + total_count -= same_digit_count / 2; + + FILE *fo = fopen("luckyagain.out", "w"); + fprintf(fo, "%lld\n", total_count); + fclose(fo); + + return 0; +} \ No newline at end of file From 4b4065d6143c2712655a95f1577374d5f32a81f7 Mon Sep 17 00:00:00 2001 From: dim131 Date: Fri, 3 May 2024 23:13:00 +0100 Subject: [PATCH 4/5] 36-luckyagain-solution --- assets/36-c-luckyagain.svg | 1 + contests/_36-PDP/c-luckyagain-solution.md | 56 +++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 assets/36-c-luckyagain.svg create mode 100644 contests/_36-PDP/c-luckyagain-solution.md diff --git a/assets/36-c-luckyagain.svg b/assets/36-c-luckyagain.svg new file mode 100644 index 00000000..8973f602 --- /dev/null +++ b/assets/36-c-luckyagain.svg @@ -0,0 +1 @@ +𝑦στοιχεία𝑥𝑖𝑥𝑖+1𝑥𝑥𝑥1𝑦1𝑦𝑦𝑥𝑖στοιχεία𝑖στοιχεία \ No newline at end of file diff --git a/contests/_36-PDP/c-luckyagain-solution.md b/contests/_36-PDP/c-luckyagain-solution.md new file mode 100644 index 00000000..bacc2bff --- /dev/null +++ b/contests/_36-PDP/c-luckyagain-solution.md @@ -0,0 +1,56 @@ +--- +layout: solution +codename: luckyagain +--- + +## Επεξήγηση εκφώνησης + +Ένας αριθμός είναι τυχερός αν έχει ζυγό αριθμό ψηφίων και τα πρώτα μισά του ψηφία έχουν το ίδιο άθροισμα με τα τελευαία μισά. + +Μας δίνεται μία λίστα από $$N$$ αριθμούς και πρέπει να βρούμε πόσα ζευγάρια από αριθμούς μπορούμε να ενώσουμε ώστε να πάρουμε έναν τυχερό αριθμό. + +## Υπολογίζοντας τα ψηφία ενός αριθμού + +Στις δύο παρακάτω λύσεις θα χρησιμοποιήσουμε τον παρακάτω αλγόριθμο για την εύρεση των δεκαδικών ψηφίων ενός αριθμού. Ο αλγόριθμος βρίσκει διαδοχικά το τελευταίο ψηφίο του αριθμού (δηλαδή το υπόλοιπο του αριθμού με το $$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$$. + - Ο αριθμός έγινε μηδέν επομένως δεν έχει άλλα ψηφία. + +Ο παρακάτω κώδικας υλοποιεί αυτόν τον αλγόριθμο και χρειάζεται χρόνο γραμμικό στο πλήθος των ψηφίων του αριθμού: + +{% 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' %}) + +## Βέλτιστη λύση + +Ας υποθέσουμε ότι για κάποιον αριθμό $$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_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$$). Χρειάζεται λίγο προσοχή με τα ζεύγη αριθμών με το ίδιο πλήθος ψηφίων, ώστε να μην τα διπλομετρήσουμε. Στον παρακάτω κώδικα η μεταβλητή $$\texttt{same\_digit\_count}$$ φροντίζει για αυτό. + + +Κάθε πρόσβαση στον πίνακα κατακερματισμού χρειάζεται $$\mathcal{O}(1)$$ χρόνο (ή $$\mathcal{O}(\log N)$$ αν χρησιμοποιήσουμε map) και επειδή κάθε αριθμός έχει το πολύ $$9$$ ψηφία, συνολικά ο αλγόριθμος χρειάζεται $$\mathcal{O}(N)$$ χρόνο. Ο κώδικας δίνεται παρακάτω. + +{% include code.md solution_name='luckyagain_efficient.cc' %} + + From 54e529404efe6fb8c7a6ee64efe1161a36926273 Mon Sep 17 00:00:00 2001 From: dim131 Date: Mon, 6 May 2024 11:16:22 +0100 Subject: [PATCH 5/5] Replies to comments --- _data/contests/36-PDP.yml | 1 + .../luckyagain/luckyagain_brute_force.cc | 2 +- .../36-PDP/luckyagain/luckyagain_efficient.cc | 19 ++++++---- contests/_36-PDP/c-luckyagain-solution.md | 38 ++++++++++++------- contests/_36-PDP/c-luckyagain-statement.md | 22 ++++++----- 5 files changed, 51 insertions(+), 31 deletions(-) diff --git a/_data/contests/36-PDP.yml b/_data/contests/36-PDP.yml index ade38ca7..4862908b 100755 --- a/_data/contests/36-PDP.yml +++ b/_data/contests/36-PDP.yml @@ -45,3 +45,4 @@ luckyagain: codes_in_git: true solution_tags: ["map", "bst", "counting", "decimal numbers", "number bases"] on_judge: false + points: 30 diff --git a/_includes/source_code/code/36-PDP/luckyagain/luckyagain_brute_force.cc b/_includes/source_code/code/36-PDP/luckyagain/luckyagain_brute_force.cc index f48d1718..94d924eb 100644 --- a/_includes/source_code/code/36-PDP/luckyagain/luckyagain_brute_force.cc +++ b/_includes/source_code/code/36-PDP/luckyagain/luckyagain_brute_force.cc @@ -6,7 +6,7 @@ typedef long long ll; bool is_lucky(const std::vector& digitsA, const std::vector& 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; // Το άθροισμα των ψηφίων του πρώτου μισού. diff --git a/_includes/source_code/code/36-PDP/luckyagain/luckyagain_efficient.cc b/_includes/source_code/code/36-PDP/luckyagain/luckyagain_efficient.cc index 0e421b1f..b0d9379a 100644 --- a/_includes/source_code/code/36-PDP/luckyagain/luckyagain_efficient.cc +++ b/_includes/source_code/code/36-PDP/luckyagain/luckyagain_efficient.cc @@ -1,9 +1,10 @@ #include -#include #include typedef long long ll; +ll counts[10 /* πλήθος ψηφίων */][82 /* άθροισμα ψηφίων */]; // Πλήθος αριθμών + int main() { FILE *fi = fopen("luckyagain.in", "r"); long N; @@ -11,8 +12,6 @@ int main() { std::vector> digits(N); std::vector digit_sum(N, 0); - std::unordered_map - counts[10 /* πλήθος ψηφίων */]; for (int i = 0; i < N; ++i) { long temp; @@ -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]; @@ -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]; } @@ -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]; } @@ -69,4 +72,4 @@ int main() { fclose(fo); return 0; -} \ No newline at end of file +} diff --git a/contests/_36-PDP/c-luckyagain-solution.md b/contests/_36-PDP/c-luckyagain-solution.md index bacc2bff..bf998c53 100644 --- a/contests/_36-PDP/c-luckyagain-solution.md +++ b/contests/_36-PDP/c-luckyagain-solution.md @@ -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$$ αντίστοιχα).
-Προϋπολογίζουμε τον πίνακα $$\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' %} diff --git a/contests/_36-PDP/c-luckyagain-statement.md b/contests/_36-PDP/c-luckyagain-statement.md index ddf456b0..541b2ae8 100644 --- a/contests/_36-PDP/c-luckyagain-statement.md +++ b/contests/_36-PDP/c-luckyagain-statement.md @@ -23,7 +23,7 @@ codename: luckyagain ## Αρχεία εξόδου: Το αρχείο εξόδου με όνομα **luckyagain.out** είναι αρχείο κειμένου που περιέχει μία μόνο γραμμή με έναν ακέραιο αριθμό: το πλήθος των διαφορετικών τρόπων που μπορούν να σχηματιστούν τυχεροί αριθμοί κολλώντας δύο χαρτάκια. -## +## Παραδείγματα **1o** @@ -31,13 +31,15 @@ codename: luckyagain | :--- | :--- | | 7
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$$. + +**2o** | **luckyagain.in** | **luckyagain.out** | | :--- | :--- | @@ -45,6 +47,8 @@ codename: luckyagain *Εξήγηση:* Δεν μπορεί να σχηματιστεί κανένας τυχερός αριθμός. +**3o** + | **luckyagain.in** | **luckyagain.out** | | :--- | :--- | | 5
17 62 35 44 80 | 20 | @@ -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.