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

34ος ΠΔΠ B' Φάση: Ο ξενοδόχος τρελλάθηκε (crazyhotel) - Λύση

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

Μας δίνεται πίνακας \(A\) με \(N\) μη αρνητικές τιμές \(A_i\) (το κόστος διαμονής της ημέρας \(i\)) και δύο τιμές ακόμα, το μέγιστο επιτρεπτό άθροισμα \(S\) και η μέγιστη επιτρεπτή τιμή \(T\). Ζητείται να βρούμε πόσοι είναι οι συνδυασμοί συνεχόμενων αριθμών με άθροισμα που δεν ξεπερνά το \(S\) και καμία τους τιμή δεν ξεπερνά το \(T\).

Παρατήρηση 1: Έστω ότι έχουμε \(k\) συνεχόμενους αριθμούς \(A_i,A_{i+1},A_{i+2},\dots,A_{i+k-1}\) που το άθροισμα τους δεν ξεπερνά το \(S\) και κανένας εξ’ αυτών δεν έχει τιμή που να ξεπερνά το \(T\). Αυτοί οι \(k\) αριθμοί, δίνουν \(k\) δυνατά διαστήματα συνεχόμενων αριθμών που να περιλαμβάνουν το \(A_i\). Αυτοί οι συνδυασμοί είναι:
\(A_i\)
\(A_i + A_{i+1}\)
\(A_i + A_{i+1} + A_{i+2}\)
\(A_i + A_{i+1} + A_{i+2} + A_{i+3}\)
\(\vdots\)
\(A_i + A_{i+1} + A_{i+2} + A_{i+3} + \dots + A_{i+k-1}\)

Οπότε αρκεί σε κάθε θέση \(\mathit{left}\), με \(1 \le \mathit{left} \le N\), να βρεθεί η μεγαλύτερη θέση \(\mathit{right}\) (\(\mathit{left} \le \mathit{right} \le N\)), ώστε το άθροισμα όλων των στοιχείων \(A_\mathit{left} + A_{\mathit{left}+1} + \dots + A_{\mathit{right}-1} + A_{\mathit{right}} \le S\) και κανένας από αυτούς τους αριθμούς να μην υπερβαίνει το \(T\).

Παρατήρηση 2: Το διάστημα \(A_\mathit{left}\) έως και \(A_\mathit{right}\), περιλαμβάνει \(\mathit{right}-\mathit{left}+1\) στοιχεία.

Μη αποδοτική λύση brute force. Πολυπλοκότητα \(\mathcal{O}(N^2)\).

Για κάθε θέση \(\mathit{left}\) εκτελούμε ένα βρόγχο while ώστε να βρούμε τη θέση \(\mathit{right}\). Αθροίζουμε τα μήκη όλων των διαστημάτων \([\mathit{left},\mathit{right}]\) που ικανοποιούν τους περιορισμούς των \(S\) και \(T\).

#include <cstdio>

const long MAXN = 1'000'000L;
long N,S,T, ans, A[MAXN+1];

int main(){
    FILE *in = fopen("crazyhotel.in","r");
    FILE *out = fopen("crazyhotel.out","w");
    fscanf(in,"%ld%ld%ld",&N,&S,&T);
    for(int i=1;i<=N;i++)
        fscanf(in,"%ld",&A[i]);
    for(long left=1;left<=N;left++){	
        //βρες την τελευταία μέρα right που μπορούμε να μείνουμε, 
        //ξεκινώντας από την ημέρα left και ξοδεύοντας το πολύ S χρήματα 
        long right = left-1, intsum = 0;
        while(right+1<=N && A[right+1]<=T && intsum+A[right+1]<=S){
            right++;
            intsum += A[right];
        }
        ans += right - left + 1;
    }
    fprintf(out,"%ld\n",ans);
    fclose(in);
    fclose(out);
    return 0;
}

Βέλτιστη λύση με χρήση δύο δεικτών (two pointers). Πολυπλοκότητα \(\mathcal{O}(N)\).

Η τεχνική αυτή χρησιμοποιεί τους δύο pointers \(\mathit{left}, \mathit{right}\) για να βρει όλα τα διαστήματα που μας ενδιαφέρουν στον πίνακα.

Στην προηγούμενη λύση, η θέση \(\mathit{left}\) αυξάνεται κατά \(1\) σε κάθε επανάληψη του εξωτερικού for μέχρι να προσπελάσει ολόκληρο τον πίνακα. Η θέση \(\mathit{right}\) αντιστοιχεί στη μεγαλύτερη θέση του πίνακα όπου \(A_\mathit{left}+A_{\mathit{left}+1}+A_{\mathit{left}+2}+\dots+A_\mathit{right} \leq S\) και κανένα στοιχείο του διαστήματος δεν υπερβαίνει το \(T\).

Στην επόμενη επανάληψη του εξωτερικού for, το \([\mathit{left},\mathit{right}]\) θα συρρικνωθεί από αριστερά, στο \([\mathit{left}+1,\mathit{right}]\) και καθώς το \(A_\mathit{left}\) που έφυγε από το επιλεγμένο διάστημα, ήταν μη αρνητικός αριθμός, το άθροισμα του συρρικνωμένου διαστήματος δεν θα μεγαλώσει. Καθώς το \(\mathit{left}\) μετακινείται προς τα δεξιά, το \(\mathit{right}\) θα μετακινείτε και αυτό προς τα δεξιά, όσο και αν χρειαστεί, ώστε να βρούμε τη νέα μέγιστη θέση και να συνεχίσουν να ισχύουν οι περιορισμοί των \(S\) και \(T\).

Συνολικά θα μετακινήσουμε κάθε δείκτη \(N\) βήματα, οπότε η πολυπλοκότητα είναι \(\mathcal{O}(N)\).

Για τον υπολογισμό του αθροίσματος κάθε διαστήματος \([\mathit{left},\mathit{right}]\) μπορούν να χρησιμοποιηθούν τα prefix sums (ή cumulative sums) όπως στον παρακάτω κώδικα:

    for(int i=1;i<=N;i++){
        fscanf(in,"%ld",&A[i]);
        PS[i] = PS[i-1] + A[i];
    }
    long left, right = 0;
    for(left=1;left<=N;left++){	
        //βρες την τελευταία μέρα right που μπορούμε να μείνουμε, 
        //ξεκινώντας από την ημέρα left και ξοδεύοντας το πολύ S χρήματα 
        if(right<left-1)//πρέπει Ν>=right+1>=L
            right=left-1;
        while(right+1<=N && A[right+1]<=T && PS[right+1]-PS[left-1]<=S)
            right++;
        ans += right - left + 1;
    }

Μπορείτε να βρείτε τον πλήρη κώδικα εδώ

ή το άθροισμα να υπολογίζεται δυναμικά (on the fly):

    long left, right=0;
    long intsum = 0;//Το άθροισμα του segment [left,right];
    for(left=1;left<=N;left++){	
        //βρες την τελευταία μέρα right που μπορούμε να μείνουμε, 
        //ξεκινώντας από την ημέρα left και ξοδεύοντας το πολύ S χρήματα 
        if(right<left-1){//πρέπει Ν>=right+1>=L
            intsum = 0;
            right=left-1;
        }
        while(right+1<=N && A[right+1]<=T && intsum+A[right+1]<=S)
            intsum += A[++right];//βάλε το right+1 στο segment
        ans += right - left + 1;
        intsum -= A[left];//το A[left] θα βγει από το segment[left,right]
    }

Μπορείτε να βρείτε τον πλήρη κώδικα εδώ

Υπάρχει μια λύση ακόμα, όπου οι δύο δείκτες δεν αναφέρονται σε πίνακα στη μνήμη, αλλά στον αποθηκευμένο στο αρχείο πίνακα. Η λύση αυτή αν και λιγότερο αποδοτική, χρειάζεται μνήμη \(\mathcal{O}(1)\) καθώς δεν δεσμεύει μνήμη για τον πίνακα \(A\). Θα χρειαστεί να δημιουργήσουμε ένα αντίγραφο του χειριστή αρχείου για να χρησιμοποιεί ο δεύτερος δείκτης (π.χ. με τη συνάρτηση dup) ή να ανοίξουμε το αρχείο δύο φορές.

    fscanf(fright, "%ld", &a_right);
    for(long right=0,left=1;left<=N;left++){	
        //βρες το right ως την τελευταία μέρα που μπορύμε να μείνουμε
        //με S χρήματα αν ξεκινήσουμε διαμονή από την ημέρα left
        if(right<left-1){//πρέπει Ν>=right+1>=L
            intsum = 0;
            ++right;
            if (right + 1 <= N) fscanf(fright, "%ld", &a_right);
        }
        while(right+1<=N && a_right <=T && intsum+a_right<=S) {
            intsum += a_right;//βάλε το right+1 στο segment
            ++right;
            if (right + 1 <= N) fscanf(fright, "%ld", &a_right);
        }
        ans += right - left + 1;
        fscanf(fleft, "%ld", &a_left);
        intsum -= a_left;//το A[left] θα βγει από το segment[left,right]
    }

Μπορείτε να βρείτε τον πλήρη κώδικα εδώ

Λύση με δυαδική αναζήτηση. Πολυπλοκότητα \(\mathcal{O}(N\dot \log {N} )\).

Καθώς οι τιμές του πίνακα είναι μη αρνητικές, ο πίνακας των prefix sum έχει αύξουσα διάταξη. Χρησιμοποιώντας το prefix sum μπορούμε να βρούμε ξεκινώντας από μια θέση \(\mathit{left}\) τη μεγαλύτερη θέση \(\mathit{right}\), όπου το prefix sum δεν θα αυξηθεί περισσότερο από \(S\). Μένει να αποφύγουμε τις απαγορευτικές τιμές που ξεπερνούν το \(T\).

Μια λύση είναι να διαβάζονται οι τιμές \(A_i\) από το αρχείο εισόδου μέχρι να φτάσουμε σε απαγορευτική τιμή ή στο τέλος του πίνακα. Κάνουμε τους υπολογισμούς στο διάστημα που προηγήθηκε και κατόπιν επαναλαμβάνουμε μέχρι να εξαντληθούν οι \(N\) τιμές. Ο κώδικας ακολουθεί:

void solve(long L,long R){
    //βρέθηκε ένα συνεχόμενο διάστημα ημερών [L,R] όπου 
    //καμία μέρα δεν ξεπερνα το κόστος T
    while(L<=R){
        long left = L, right = R, pos = -1, target = S + PS[L-1];
        //pos θα είναι η δεξιότερη μέρα όπου PS[pos]<=target
        while(left<=right){
            int mid = (left+right)/2;
            if(PS[mid]<=target){
                pos = mid;
                left = mid+1;
            }else{
                right = mid-1;
            }
        }
        //το pos είναι η δεξιότερη άκρη του διαστήματος στο 
        //οποίο ξεκινώντας απο ημέρα L, ξοδεύουμε μέχρι S
        if(pos>=0)
            ans += (pos - L + 1);
        L++;
    }
}

Μπορείτε να βρείτε τον πλήρη κώδικα εδώ

Μια άλλη λύση για το binary search που να ξεπερνά το εμπόδιο των απαγορευτικών τιμών, είναι να αυξηθεί η τιμή των ημερών αυτών σε \(S+1\). Με τον τρόπο αυτό, η prefix sum που ψάχνει τη μεγαλύτερη αύξηση από τη θέση \(\mathit{left}\) που όμως να μην ξεπερνά το \(S\), δεν θα συμπεριλάβει ποτέ τέτοιο σημείο. Προσοχή στην υλοποίηση αυτή χρειάζεται ο τύπος ακεραίων που θα χρησιμοποιηθεί στον πίνακα prefix sum. Εφόσον ενδέχεται να έχουμε έως \(N=1.000.000\) απαγορευτικές τιμές και με μέγιστο \(S=1.000.000.000\), το prefix sum θα πρέπει να χωρά τον αριθμό \(10^6\cdot (10^9+1)\), άρα θα χρειαστούμε ακεραίους 64bit.

    for(long i=1;i<=N;i++){
        fscanf(in,"%ld",&A[i]);
        if(A[i]>T) A[i] = S+1;
        PS[i] = PS[i-1] + A[i];
    }
    for(int i=1;i<=N;i++){
        long left = i, right = N, pos = -1;
        long long target = PS[i-1] + S;
        while(left<=right){
            int mid = (left+right)/2;
            if(PS[mid]<=target){
                pos = mid;
                left = mid+1;
            }else{
                right = mid-1;
            }
        }
        if(pos>=0)
            ans += (pos - i + 1);
    }

Μπορείτε να βρείτε τον πλήρη κώδικα εδώ