diff --git a/_data/contests/36-PDP.yml b/_data/contests/36-PDP.yml index 81462eef..4862908b 100755 --- a/_data/contests/36-PDP.yml +++ b/_data/contests/36-PDP.yml @@ -34,3 +34,15 @@ allthatjazz: solution_tags: ["two pointers","sorting","binary search"] on_judge: true +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 + points: 30 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..94d924eb --- /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..b0d9379a --- /dev/null +++ b/_includes/source_code/code/36-PDP/luckyagain/luckyagain_efficient.cc @@ -0,0 +1,75 @@ +#include +#include + +typedef long long ll; + +ll counts[10 /* πλήθος ψηφίων */][82 /* άθροισμα ψηφίων */]; // Πλήθος αριθμών + +int main() { + FILE *fi = fopen("luckyagain.in", "r"); + long N; + fscanf(fi, "%ld", &N); + + std::vector> digits(N); + std::vector digit_sum(N, 0); + + 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; // -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; +} 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..bf998c53 --- /dev/null +++ b/contests/_36-PDP/c-luckyagain-solution.md @@ -0,0 +1,68 @@ +--- +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$$. + - Ο αριθμός έγινε μηδέν επομένως δεν έχει άλλα ψηφία. + +Ο παρακάτω κώδικας υλοποιεί αυτόν τον αλγόριθμο και χρειάζεται χρόνο γραμμικό στο πλήθος των ψηφίων του αριθμού:[^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 %} + +Συνολικά ελέγχουμε $$Ν \cdot (N - 1)$$ ζευγάρια και κάθε έλεγχος χρειάζεται το πολύ σταθερό αριθμό πράξεων (γιατί τα ψηφία κάθε αριθμού είναι το πολύ $$9$$). Μπορείτε να βρείτε ολόκληρο τον κώδικα [εδώ]({% include link_to_source.md solution_name='luckyagain_brute_force.cc' %}) + +## Βέλτιστη λύση + +Ας υποθέσουμε ότι για κάποιον αριθμό $$x$$ θέλουμε να μετρήσουμε όλους τους αριθμούς $$y$$ για τους οποίους ισχύει ότι η ένωση τους $$xy$$ είναι τυχερός αριθμός. Θέλουμε να το κάνουμε αυτό χωρίς να πρέπει να δοκιμάσουμε όλους τους δυνατούς αριθμούς $$y$$. + +Το ποια ψηφία του $$x$$ συνεισφέρουν στο πρώτο και ποια στο τελευταίο μισό του αριθμού $$xy$$, εξαρτάται από το που βρίσκεται το μέσο τους. Ας θεωρήσουμε ότι το μέσο τους βρίσκεται αμέσως μετά το $$i$$-οστό ψηφίο του $$x$$, όπως στο ακόλουθο σχήμα (όπου $$\ell_x$$ και $$\ell_y$$ είναι το πλήθος των ψηφίων του $$x$$ και $$y$$ αντίστοιχα). + +
+ +
+ +Τότε, ψάχνουμε για όλους τους αριθμούς $$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~\cdot$$ και μία όταν ψάχνουμε για αριθμούς της μορφής $$\cdot~y$$. Στον παρακάτω κώδικα η μεταβλητή $$\texttt{same\_digit\_count}$$ μετράει πόσες τέτοιες περιπτώσεις υπάρχουν και αφαιρεί τις μισές από αυτές στο τέλος. Πρέπει επίσης να προσέξουμε να μην μετρήσουμε τα ζευγάρια της μορφής $$xx$$. + + +Κάθε πρόσβαση στον πίνακα χρειάζεται $$\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 new file mode 100644 index 00000000..541b2ae8 --- /dev/null +++ b/contests/_36-PDP/c-luckyagain-statement.md @@ -0,0 +1,68 @@ +--- +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$$. + +**2o** + +| **luckyagain.in** | **luckyagain.out** | +| :--- | :--- | +| 6
265 10387 392 981 6986 74 | 0 | + +*Εξήγηση:* Δεν μπορεί να σχηματιστεί κανένας τυχερός αριθμός. + +**3o** + +| **luckyagain.in** | **luckyagain.out** | +| :--- | :--- | +| 5
17 62 35 44 80 | 20 | + +*Εξήγηση:* Κολλώντας οποιαδήποτε χαρτάκια προκύπτει πάντα τυχερός αριθμός. + +## Περιορισμοί: + - $$1 \leq N \leq 1.000.000$$, + - Οι αριθμοί θα είναι το πολύ εννιαψήφιοι και το πρώτο ψηφίο θα είναι πάντα μη μηδενικό. + - Για περιπτώσεις ελέγχου συνολικής αξίας 30%, θα είναι $$N \leq 1.000$$. + - Για περιπτώσεις ελέγχου συνολικής αξίας 50%, οι αριθμοί θα είναι πενταψήφιοι ή εξαψήφιοι. + +**Προσοχή!** Η απάντηση μπορεί να υπερβαίνει το $$2^{32}$$. Επίσης, φροντίστε να διαβάζετε την είσοδο και να εκτυπώνετε την έξοδο αποδοτικά, ειδικά αν προγραμματίζετε σε C++ ή Java. + +**Μορφοποίηση:** Στην έξοδο, όλες οι γραμμές τερματίζουν με ένα χαρακτήρα newline.
+**Μέγιστος χρόνος εκτέλεσης:** 2 sec.
+**Μέγιστη διαθέσιμη μνήμη:** 128 MB.