Αρχική > 35ος ΠΔΠ > chemicals (εκφώνηση)

35ος ΠΔΠ Γ' Φάση: Χημικές Ενώσεις (chemicals) - Λύση

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

Μας δίνεται ένας πίνακας \(A\) από \(N\) ακεραίους αριθμούς και ένας ακέραιος αριθμός \(K\). Μας ζητείται να βρούμε το μήκος του μεγαλύτερου διαστήματος \([i, j]\) ώστε να μην υπαρχει ζεύγος \(s, t\) στο \([i, j]\) με άθροισμα \(A[s] + A[t]\) που να είναι πολλαπλάσιο του \(K\).

Brute force \(\mathcal{O}(N^4)\)

Υπάρχουν συνολικά \(\mathcal{O}(N^2)\) διαστήματα \([i, j]\) και κάθε διάστημα έχει το πολύ \(\mathcal{O}(N^2)\) ζευγάρια. Επομένως, μπορούμε να ελέγξουμε ένα ένα τα διαστήματα συνολικά σε \(\mathcal{O}(N^4)\) χρόνο.

#include <algorithm>
#include <cstdio>

const size_t MAXN = 1'000'000;
long A[MAXN];

int main() {
   FILE *fi = fopen("chemicals.in", "r");
   long N, K;
   fscanf(fi, "%ld%ld", &N, &K);
   for (long i = 0; i < N; ++i) {
      fscanf(fi, "%ld", &A[i]);
   }
   fclose(fi);
   
   long max_range = 0;
   // Δοκιμάζουμε όλα τα δυνατά διαστήματα [i, j].
   for (long i = 0; i < N; ++i) {
      for (long j = i; j < N; ++j) {
         // Δοκιμάζουμε όλα τα δυνατά ζεύγη (s, t) στο [i, j].
         bool found_pair = false;
         for (long s = i; s <= j && !found_pair; ++s) {
            for (long t = s + 1; t <= j && !found_pair; ++t) {
               if ((A[s] + A[t]) % K == 0) {
                  found_pair = true;
               }
            }
         }
         if (!found_pair) {
            max_range = std::max(max_range, j - i + 1);
         }
      }
   }
   
   FILE *fo = fopen("chemicals.out", "w");
   fprintf(fo, "%ld\n", max_range);
   fclose(fo);
   
   return 0;
}

Brute force \(\mathcal{O}(N^3)\)

Μπορούμε να επιταγχύνουμε την παραπάνω λύση ως εξής: ξέροντας ότι το διάστημα \([i, j-1]\) δεν έχει κάποιο ζευγάρι που να είναι πολλαπλάσιο του \(K\), τότε για το διάστημα \([i, j]\) αρκεί να ελέγξουμε ότι τα ζευγάρια \((i, j), (i+1, j), \ldots, (j-2, j), (j-1, j)\) έχουν ζευγάρια που δεν είναι πολλαπλάσια του \(K\). Επειδή υπάρχουν το πολύ \(\mathcal{O}(N)\) ζευγάρια με το \(j\), ο αλγόριθμος παίρνει συνολικά \(\mathcal{O}(N^3)\) χρόνο.

   for (long i = 0; i < N; ++i) {
      for (long j = i; j < N; ++j) {
         // Δοκιμάζουμε όλα τα δυνατά ζεύγη (k, j) στο [i, j].
         bool found_pair = false;
         for (long k = i; k < j && !found_pair; ++k) {
            if ((A[k] + A[j]) % K == 0) found_pair = true;
         }
         if (!found_pair) {
            max_range = std::max(max_range, j - i + 1);
         } else {
            // Αν βρούμε ζευγάρι που δεν είναι καλό, τότε βγαίνουμε από το loop.
            break;
         }
      }
   }

Ολόκληρος ο κώδικας είναι εδώ.

Brute force με hash set \(\mathcal{O}(N^2)\)

Mπορούμε να επιταγχύνουμε περαιτέρω την παραπάνω λύση, κάνοντας την εξής παρατήρηση:

Παρατήρηση: Δύο στοιχεία \(A[s]\) και \(A[t]\) έχουν άθροισμα πολλαπλάσιο του \(K\) αν και μόνο αν \(A[s] \equiv (K - A[t]) \pmod{K}\).

(Αιτιολόγηση) Μπορούμε να ελέγξουμε ότι \(A[s] + A[t]\) είναι πολλαπλάσιο του \(K\), καθότι \((A[s] + A[t]) \equiv ((K - A[t]) + A[t]) \equiv 0 \pmod{K}\).

Επομένως, διατηρώντας σε ένα hash set \(S\) τα στοιχεία \(A[i], \ldots, A[j-1]\), μπορούμε να ελέγξουμε γρήγορα αν το στοιχείο \(A[j]\) συμμετέχει σε κάποια δυάδα με άθροιμα \(A[\ell] + A[j]\) που είναι πολλαπλάσιο του \(K\), κοιτώντας αν υπάρχει το στοιχείο \((K - (A[j] \% K) ) \% K\) στο \(S\).1

   // Δοκιμάζουμε όλα τα δυνατά διαστήματα [i, j].
   for (long i = 0; i < N; ++i) {
      std::unordered_set<long> rems;
      bool found_pair = false;
      long j;
      for (j = i; j < N; ++j) {
         if (rems.find( (K - (A[j] % K) ) % K ) != rems.end()) {
            found_pair = true;
            break;
         }
         rems.insert(A[j] % K);
      }
      max_range = std::max(max_range, k - i);
   }

Κάθε πρόσβαση στο hash set θέλει \(\mathcal{O}(1)\) χρόνο, επομένως συνολικά ο αλγόριθμος θέλει \(\mathcal{O}(N^2)\) χρόνο. Ολόκληρος ο κώδικας είναι εδώ.

Δύο δείκτες \(\mathcal{O}(N)\)

Γενικεύοντας την προηγούμενη λύση, μπορούμε να υπολογίσουμε για κάθε δείκτη \(j\) το μεγαλύτερο διάστημα \([\ell, j]\) για το οποίο δεν υπάρχει ζεύγος \(t, s \in [\ell, j]\) με άθροισμα \(A[s] + A[t]\) που δεν είναι πολλαπλάσιο του \(K\). Έχοντας την βέλτιση λύση για το \(j-1\) έστω \([\ell, j-1]\), μπορούμε να υπολογίσουμε την βέλτιση λύση για το \(j\) μετακινώντας το \(\ell\) έως ότου προσπεράσουμε όλα τα στοιχεία ίσα με \((K - (A[j] \% K) ) \% K\).

   // Δοκιμάζουμε όλα τα δυνατά διαστήματα [i, j].
   std::unordered_map<long, long> rems;
   long left = 0;
   for (long i = 0; i < N; ++i) {
      long target = (K - (A[i] % K) ) % K;
      while (rems[target] > 0) {
         --rems[A[left] % K];
         ++left;
      }
      ++rems[A[i] % K];
      max_range = std::max(max_range, i - left + 1);
   }

Για το δεύτερο παράδειγμα, οι δύο δείκτες αλλάζουν ως εξής, δίνοντας το μέγιστο στο 5ο βήμα:

2ο παράδειγμα για 2 δείκτες

Μπορούμε να μετακινήσουμε τον δείκτη \(\ell\) το πολύ \(\mathcal{O}(N)\) φορές (δηλαδή το εσωτερικό \(\texttt{while}\) θέλει \(\mathcal{O}(N)\) χρόνο), επομένως ο αλγόριθμος χρειάζεται συνολικά \(\mathcal{O}(N)\) χρόνο. Ολόκληρος ο κώδικας εδώ.

  1. Το εσωτερικό mod χρειάζεται ώστε η διαφορά να μην βγει αρνητική.