Editorial for Mine
Submitting an official solution before solving the problem yourself is a bannable offence.
Önbilgi
Bu soruda istenen n uzunluğundaki bir vektördeki toplamı en büyük olacak k adet subarrayi seçmek.
Soruyu çözerken aynı soruyu k subarray yerine 1 subarray için çözen Kadane'nin Largest Sum Contiguous Subarray(En Büyük Toplamlı Subarray) algoritmasından yararlanacağız.
Kadane Algoritması array'i soldan sağa gezip her adımda üstünde olduğu nokta sağ ucu olan en büyük subarrayi hesaplayıp o noktaya kaydeder. bunu da 2 adımda şu şekilde yapıyoruz:
- İlk adımda arrayin ilk noktası oluşabilecek en büyük subarray olduğundan o noktadaki değeri kaydediyoruz.
- Daha sonra arrayin geri kalanını dolanıp arrayin bir önceki noktasındaki subarrayin
kaydedilmiş olan puanına bakıyoruz
Toplam n adımda algoritma son buluyor.eğer pozitifse kendi puanını önceki toplama ekliyor. eğer negatifse sadece kendi puanını kullanıyor.
Not: 1 kadane subarrayin sağında ve solunda 0'lardan sonra negatif bir sayı ilk başta pozitif bir sayı gelcektir bunun nedeni öyle bir durumda negatif sayıdan sonraki kısmın seçileceğidir örnek: subarray 0 0 0 -1 3 5 0 seçmek yerine 3 5 0 veya 3 5 seçilir. örnek: subaray 1 5 2 -1 0 0 seçmek yerime 1 5 2 seçilir
Çözüm
Bu soruyu optimize etmek için toplam 4 aşamada çözdük(complexity değişmeyecek olsa bile bizim çözdüğümüzden daha optimize hale de getirilebilir) ve çözerken kadane algoritmasını kullandık. Fakat biz bu adımların hepsine değinmek yerine ana hatlarıyla şu şekilde özetleyeceğiz(eğer optimize ettiğimiz yola bakmak isterseniz alttaki kodun açıklamalarıyla okumanızı öneririm):
Soruyu sırasıyla 1-subarray,2-subarray,...,k-subarray için çözeceğiz
Bunu da her adımda arrayi parçalara ayırarak yapıyoruz. Elimizdeki en büyük kadane array toplamına(bunlar daha her adımda hesaplanmak yerine oluşturulduğunda hesaplanıp kaydediliyor) sahip arrayi bulup(ilk adımda tek bir arrayimiz var o da girdide verilen N uzunluğundaki array) 3 veya daha az sayıda parçalara ayırarak bölüyoruz. Bu 3 parçadan 1 tanesi, arrayin kadane subarrayi diğer ikisi kadane subarrayin sağında ve solunda kalan subarrayler.
- Örnek 1: [1,-5,3,-1,5,2,-3,2] arrayi için [1,-5] (seçilmemiş),[3,-1,5,2] (seçilmiş), [-3,2] (seçilmemiş) olarak ekleniyor.
- Örnek 2: [3,-1,5,2,-3,2] arrayi içinse sadece 2 array ekleniyor [3,-1,5,2] (seçilmiş), [-3,2] seçilmemiş olarak ekleniyor.
- Not: Bu arrayler priority queue da içlerindeki kadane arraylerinin toplamlarına göre sıralanmakta.
Yukarıdaki örnekler priority queue'dan gelen arrayin seçilmemiş olma durumu için geçerli. Bu array seçilmemiş olduğundan içindeki kadane subarray toplamımızı arttıracağı için 1 seçilmemiş arrayi 1 seçilmiş 2 seçilmemiş arraye bölüyoruz. Fakat eğer priority queuedan seçilmiş gelirse ne yapacağız?
Eğer priority queue dan seçilmiş bir array gelirse içindeki toplamı en küçük olan yani negatif kadane subarrayini bulup bu arrayi 2 adet seçilmiş ve 1 adet seçilmemiş arraye parçalıyoruz(negatif kadane subarray toplamı düşürüceği için onu çıkarmış oluyoruz).
- Örnek: [3,-1,5,2] seçilmiş subarrayi geldiğinde bunu [3] (seçilmiş), [-1] (seçilmemiş), [5,2] (seçilmiş) arrayler olmak üzere 3 arraye bölüp tekrardan priority queue ya ekliyoruz.
Bu işlemleri k kere uygulayınca algoritmamız sona eriyor ve cevabı buluyoruz.
Önemli Not: Priority Queue ya atılan yapılar içlerinde arrayleri ve bu arraylerin kadane subarraylerini barındırmak yerine(işlem sayısını arttırmamak adına) onların(main-array ve kadane subarrayin) indexlerini ve kadane subarraylerinin toplamını tutuyor(ve seçilip seçilmediği bilgisini). Array yazmamızın nedeni anlatımı kolaylaştırmaktı.
Önemli Bilgiler
- Arrayleri priority queue ya atmadan önce kadane arraylerini hesaplayıp atıyoruz.
- Eğer seçilmiş bir array ise negatif kadane subarrayini hesaplıyoruz ve sonucu -1 le çarpıp kaydediyoruz(bunun nedeni kadane subarray çıkalıcağı için en düşük toplamlı arrayi bulması. Bulduğu bu array çıkarılacağı için toplamı da ana-toplamdan çıkarılacak.)
- Bir arrayi priority queueya atmadan önce kadane arraylerini hesaplıyoruz. Böylece her adımda kadane arraylerini hesaplamamıza gerek kalmıyor.
- Bir arrayi priority queueya atmadan önce kadane array toplamını kontrol ediyoruz. Eğer negatif çıkarsa bu değer toplamımızı arttırmayacağı tam tersine azaltacağı için priority queue ya eklemiyoruz.
- Eğer seçilmiş bir arrayin kadane array toplamı negatif değilse bu array sadece pozitif değerler içeriyordur.
- Eğer seçilmemiş bir arrayin kadane array toplamı negatif değilse bu array sadece negatif değerler içeriyordur.
- Bir arrayi priority queueya atmadan önce kadane array toplamını kontrol ettiğimizde 0 çıkarsa.
- Bu array seçilmemişse priority queue ya ekliyoruz çünkü seçilince hem k sayısını arttırıcak hem de toplamımızı düşürmeyecektir.
- Seçilmişse ekleyip eklememek implamantasyona bağlı. Biz daha sonraki adımlarda parçaladığımız için eklememeyi tercih ettik.
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
int n,k;
int result=0;
vector<int> ARR; //main array
vector<int> elementsInsideGroup; //will be used in part 3 if neccessary it records element count of every group
class SUBARRAY{
public:
bool selected;
/*
selected==false means it is not a chosen subarray (is not a part of k)
selected==true means it is a chosen subarray (is a part of k)
*/
int sum;
/* if
selected==false then sum will represent the pozitive kadane subarray of this subarray
selected==true then sum will represent the negative kadane subarray of this subarray
*/
int first,second;
//first and second are the boundaries of the subarray
int inFirst,inSecond;
//inFirst/Second are the boundaries of the kadane subarray(neg kadane for selected SUBARRAYs)
bool operator<(const SUBARRAY& rhs){return sum<rhs.sum;}
SUBARRAY(int f,int s,bool x){
first=f;
second=s;
selected=x;
kadane();
}
bool operator<(const SUBARRAY &rhs)const{return sum<rhs.sum;}
void kadane(){
/*
If this array is a selected array
than kadane function will find its minimum sum subarray (negative kadane subarray)
if it is not a selected array
than kadane function will find its maximum sum subarray
*/
int N=second-first+1; //number of elements of the subarray
int groupLengthMax=N; //will be used for choosing between equal sums
//we will try to optimize our solutions with these
int tempFirst=first,tempSecond=first;
//temps will be used for saving current indexes
//we will save them to in variables each time a better solution is found
inFirst=inSecond=first;
int prev,current;
sum=current=ARR[first];
for(int i=1;i<N;i++){
prev=current;
if( (!selected&&prev>=0) || (selected&&prev<=0) ){
current=prev+ARR[first+i];
tempSecond++;
}
else{
current=ARR[first+i];
tempFirst=tempSecond=first+i;
}
if( (!selected&&sum<current) || (selected&&sum>current) ){
sum=current;
inFirst=tempFirst;
inSecond=tempSecond;
groupLengthMax=max(tempFirst-first,max(tempSecond-tempFirst+1,second-tempSecond));
}
else if(sum==current){ //used for optimization
int temp_groupLengthMax=max(tempFirst-first,max(tempSecond-tempFirst+1,second-tempSecond));
if(groupLengthMax>temp_groupLengthMax){
groupLengthMax=temp_groupLengthMax;
inFirst=tempFirst;
inSecond=tempSecond;
}
}
}
if(selected)sum=-sum; //We will reverse its sum for negative kadane subarrays because the it will generally be negative
//and since it will be unselected later on, we will try to increase the sum by exluding this part of the array
}
};
int main(){
ios_base::sync_with_stdio(false); cin.tie(NULL);
cin>>n>>k;
vector<int> NEGATIVES; //will be used in part 4 if neccessary. it contains every negative element
//------------------1st part------------------
int temp,current_sum,element_count=1;
cin>>temp;
current_sum=temp;
if(temp<0)NEGATIVES.push_back(temp); //Important for part 4
bool positive=temp>=0; //it is actually checking non-negativeness but this seemed easier to write
for(int i=1;i<n;i++){
cin>>temp;
if(temp<0)NEGATIVES.push_back(temp); //Important for part 4
if(i==n-1){ //adding last subarray
if(positive==(temp>=0)){
current_sum+=temp;
element_count++;
ARR.push_back(current_sum);
elementsInsideGroup.push_back(element_count);
}
else{
ARR.push_back(current_sum);
elementsInsideGroup.push_back(element_count);
element_count=1;
ARR.push_back(temp);
elementsInsideGroup.push_back(element_count);
}
}
else if(positive==(temp>=0)){
//if the group and temp are both non-negative or both negative
//that group is not over so continue to add the temp to it
current_sum+=temp;
element_count++;
}
else{
//if those are non-negative to negative our segment is done and we will record it and number of elements it contains
positive=!positive;
ARR.push_back(current_sum);
current_sum=temp;
elementsInsideGroup.push_back(element_count);
element_count=1;
}
}
n=ARR.size();
/*
at part1 we are only changing the array for a more optimized version
by just traversing our inputs once which means it is a linear process
so complexity of part 1 is O(n)
Note: from this point on n that we use in the code will represent the group count
which again can be any number from 1 to n
*/
//--------------------------------------------
priority_queue<SUBARRAY> part2pq; //for part 2
list<SUBARRAY> part3list; //for part 3
part2pq.push(SUBARRAY(0,n-1,false));
if(part2pq.top().sum<0)part2pq.pop(); //go to part 2 if the array only contain negative elements
//------------------2nd part------------------
//choosing from Kadane subarrays until we can not increase our result
//or we reach k subarrays
int i=0;
for(;i<k;i++){
if(part2pq.empty())break;
SUBARRAY current=part2pq.top();
part2pq.pop();
if(current.selected){
result+=current.sum;
if(current.first!=current.inFirst){
//it will always exist because
//our previously selected subarray will always start with positives and end with positives
SUBARRAY next=SUBARRAY(current.first,current.inFirst-1,true);
if(next.sum<=0)part3list.push_back(next); //if it can not increase the current solution add to the part2
else part2pq.push(next);
}
if(current.second!=current.inSecond){
//it will always exist because
//our previously selected subarray will always start with positives and end with positives
SUBARRAY next=SUBARRAY(current.inSecond+1,current.second,true);
if(next.sum<=0)part3list.push_back(next); //if it can not increase the current solution add to the part2
else part2pq.push(next);
}
SUBARRAY next=SUBARRAY(current.inFirst,current.inSecond,false);
if(next.sum>=0)part2pq.push(next); //if it can increase or maintain the current solution
/* NOTE:
By saying if it can increase or maintain the current solution we re saying one of two things
1- For selected subarrays if it can increase solution would suggest that:
it has at least one negative element.
2- For unslected subarrays if it can increase or maintain current solution would suggest that:
it has at least one non-negative element.
*/
}
else{ //if not selected
result+=current.sum;
if(current.first!=current.inFirst){ //if it exists add left part
SUBARRAY next=SUBARRAY(current.first,current.inFirst-1,false);
if(next.sum>=0)part2pq.push(next); //if it can increase or maintain the current solution
}
if(current.second!=current.inSecond){ //if it exists add right part
SUBARRAY next=SUBARRAY(current.inSecond+1,current.second,false);
if(next.sum>=0)part2pq.push(next); //if it can increase or maintain the current solution
}
SUBARRAY next=SUBARRAY(current.inFirst,current.inSecond,true);
if(next.sum<=0)part3list.push_back(next); //if it can not increase the current solution add to the part2
else part2pq.push(next);
}
}
/* AT THIS POINT WE HAVE 2 POSSIBILITIES
1- part1 ended because of i=k and since this is the case we have chosen every subarray we need
so the algorithm is over.
2- part1 ended because part2pq was empty which means:
every subarray (both selected and unselecteds) did not have a kadane subarray that can either increase
or maintain our current solution. Which means:
- every non-negative groups are already inside a selected subarray
- and every negative group is not inside a selected subarray
*/
//--------------------------------------------
//------------------3rd part------------------
/* if the algorithm is at this point we can not get a better solution
so we would instead try to maintain our solution by dividing the selected subarrays. For example:
If we have a selected subarray of [1,4,2,5,0,1] where its groups contain [3,4,4,4,4,5] elements inside of them
It means that there are 24 elements in total in this group so we can approach k by 23 in 6 operations
and that is what part 3 does
Note:since every selected group is disjoint worst case complexity of part 3 is O(k)
because at every step we will divide the selected subarray into 1 element parts
until we get k groups
Note 2: the worst case is if every selected subarray we have has 1 group and every group has only 1 element
it will count every element group and then stop but
since selected group count will be less then k its worst case complexity is still O(k)
*/
while(i<k){
if(part3list.empty())break;
SUBARRAY current=part3list.front();
part3list.pop_front();
i--;
for(int x=current.first;i<k && x<=current.second;x++){
i+=elementsInsideGroup[x];
}
}
if(i>=k){
cout<<result<<endl;
return 0;
}
//--------------------------------------------
//------------------4th part------------------
/* If the code comes to this part it means that:
- Every selected subarray contains only 1 elemets agn tod those elements are positive or 0. For example [12].
- and every non-selected number is negative
which means to choose the remaining ones the best algorithm is to sort the ones remaining
and add the highest ones to our result (do not forget highest negative integer is -1)
Note: worst complexity of part 4 is O(k+nlogn) or O(nlogn)
*/
sort(NEGATIVES.begin(), NEGATIVES.end(), greater<int>());
for(int x:NEGATIVES){
if(i>=k)break;
i++;
result+=x;
}
//adding compulsary negative entries as subarrays
//--------------------------------------------
cout<<result<<endl;
}