前端時間之前(大概有兩周了吧)我們進行了第一次acm賽制的模擬比賽(雖然最後總分爆零了但還是學到了很多東西滴)
t1的題目為約瑟夫問題
我當時一看這個問題就感到十分狂喜,這t1果然是最簡單的肯定能切了呀!
(然而我還是沒有體會到社會的陰暗)
題面要求是n個人數到m槍斃,然後要輸出最後的倖存者編號
資料範圍:1≤n≤1e9,1≤m≤1e5
當時我一看這個資料範圍就直接懵掉,如果是這個資料範圍的話那麼時間複雜度必須要控制在o(mlogn)以內
事實證明最優演算法的時間複雜度確實是o(mlogn)
ros是通過oiwiki上的詞條學習的約瑟夫問題:戳這裡
ros只是講一講思想並且把**貼在這裡
沒什麼好說的,就是模擬。可以在底層迴圈中運用膜的相關知識進行相關優化
詳細過程可以見我在上面放的oiwiki鏈結
(此處先簡單表述一下之後更改)我們用線段樹維護一段區間內的還生存的人的數目。然後因為我們要m人槍斃乙個,所以我們便要從此結點往後找和為m的節點,如果仍然小於就回到開頭,仍然小於就%一下之後繼續尋找(也可以先%貌似更合理一些)
(因為覺得這種方法**十分得長並且時間複雜度還很高所以就沒有寫)
大概思路見oiwiki
需要注意的是由於我們在之前已經槍斃了的人是不能計數的所以我們要將這些人跳過之後重新計數
所以就要運用一些數學知識了:
我們將有i個人參與遊戲從1開始編號的勝者編號記為f[i]
假設f[i]已知,我們現在要求f[i+1]
如何求解?
我們在此o(n)遞推演算法下是一次操作只計算乙個人,即我們要每次只能處決乙個人。
所以我們有在f[i+1]的狀態下我們一定有在這一層的計算下編號為m%(i+1)的人一定是要被淘汰的
所以我們知道在f[i]層編號為f[i]得點是最終的倖存者,於是在f[i+1]層中我們很容易得到勝者的編號為(m%(i+1)+f[i])%(i+1)
正確性顯然,省略證明
值得注意的是這種演算法雖然時間複雜度優秀但是只能計算最後乙個人卻不能依次輸出被淘汰的人的編號。如果你要輸出依次出圈的人的編號,請用1和2演算法
並且編號可以從0開始或者從1開始,從0開始的話在%的過程中會簡便很多但是ros因為習慣問題一般採用從1編號(二者均可)
並且需要注意的是注意的是,由於單純的o(n)線性演算法的時間複雜度仍然很高所以需要進行相關的優化(之後再講),這個**就是進行了相關優化的**。如果不進行相關優化而只寫樸素的o(n)演算法的話則仍然會爆掉大資料(只有20分)
(由於我一開始學會的是更加優秀的o(mlogn)演算法所以關於o(n)演算法的相關優化並沒有仔細研究,想要了解的話可以上網自行查閱
一開始我先看懂的是這個演算法,整體思路還是很容易理解並且很清晰的。
johnsef(x,t)表示現在場上剩餘x個人,我們需要數到t時處決乙個人
那麼我們在函式內部可以分情況討論
當x>t的時候我們得知所有小於x的t的倍數都應該被處決(從1編號)
但是由於資料範圍的限制我們不能用乙個vis陣列儲存而通過運算使得計算的時候跳過去。
如何實現?
我們首先看一下johnsef函式的**並討論x>=t的情況假設我們已經將所有小於x的t的倍數全部篩去
那麼我們知道我們應該篩去x/t個數字
那麼他的子問題的解就是johnsef(x-x/t,t)
那麼我們先用乙個int型別變數res儲存一下johnsef(x-x/t,t)
之後我們得知在這個母問題中(從1編號)所有t的倍數的點全部被篩掉了
觀察**我們可以明白
首先我們需要判斷這個數字是不是末尾的x%t數中的一位
實現方法就是先將這個子問題的解減去x%t,如果這個值大於0那麼說明倖存者的編號前x/t*t個數中的乙個,並且編號就是這個數
否則說明倖存者的編號在在最後的x%t個數字中
那麼我們應該加上n就得到了在這個母問題中的倖存者編號
為什麼加上n之後就是倖存者編號了?
感性理解:因為我們在母問題中一共有n個人,我們將隊尾的人減去了。然而此時運算之後的差值是小於零的,所以有我們需要將結果加上x才是從x/t*t之後數字的正確編號
理性證明:假設子問題的解減去x%t為tmp,那麼當tmp小於零的時候我們需要將x%t加回去並且再加上x/t*t;而因為x%t==x-x/t*t,所以有tmp-x%t+x==tmp-(x-x/t*t)+x==tmp+x/t*t,得到的這個值也就是我們要求的母問題的編號了!
那麼我們如何跳過已經被處決的人呢?
我們知道每到達乙個編號為m的人我們就要進行一次處決
所以我們可以將m個人分成1組
而當我們進行子任務的求解之後假設在母任務的編號是在前x/t*t個數字之中,那麼我們有沒t-1個數字可以分成一組,我們只需要看其為哪一組就可以了!如果是第二組的話就需要加上1,是第i組的話我們就需要加上i-1(這是用於理解的方法)
**中只需要加上(res-1)/(t-1)就可以了(因為從1開始編號所以我們需要將res減去1)
然後我們來討論一下x
**:
#include#includeusing
namespace
std;
intt;
intn,m;
int johnsef(int x,int
t)
int res=johnsef(x-x/t,t);
res-=x%t;
if(res<=0) res+=x;
else res+=(res-1)/(t-1
);
return
res;
}int
main()
return0;
}
由於遞迴演算法會浪費大量的空間,我們我們可以用類似遞推的方法來求得該類問題的解
總體思路與方法4很類似
(此方法ros從0開始編號)
同樣用res儲存一下我們在該曾迴圈中找到的倖存者編號
因為我們可以得到f[i]=(f[i-1]+m)%i
所以類似方法4我們可以一下子跳過i/m個被處決的人
依然是列舉當前的人數i
令i==res+m*x,有x==(i-res)/m
所以有我們可以一下子跳過x個人,但是我們極有可以跳過x個人之後發現人數大於n了(即跳過了我們要求的答案)
所以我們需要進行一步判斷防止在內部跳過最多人數
需要注意的是,這種演算法是對方法4的遞迴優化,當n遠遠大於m的時候十分優秀。但如過n小於m的話,其時間複雜度與方法三的o(n)沒有什麼區別了(其實其他方法也是)
因為在這道題中ros從0開始編號所以我們需要對最後結果加上1才能得到題目要求的編號
ac**:
#include#includeusing
namespace
std;
intn,m;
intt;
intmain()
else
}res=(res+m)%i;
}printf(
"%d\n
",res+1
); }
return0;
}
the end.
約瑟夫問題詳解
name 約瑟夫環 模擬 author 李帥 date 10 11 08 23 53 description 約瑟夫問題 一群人圍成一圈,這群人共有 n個人,每個人身上都乙個value,依次給這圈人編號 1,2,n 一開始報數的上限值為m從第乙個人 編號 1 自一開始報數報到m時停止報數,報到m的人...
約瑟夫問題詳解
約瑟夫問題記得是在學習c語言陣列的時候寫過的一道題目,至於什麼是約瑟夫問題我想大多數學過c的人都應該知道的,下面就來說下如何利用陣列解決約瑟夫問題。預設的約瑟夫問題是從第乙個人開始計數,並且留下最後乙個倖存者,筆者做的改進是可以從指定位置起始計數,並且可以留下指定的人數,並且可以檢視被踢出的人。最開...
約瑟夫環問題詳解
講乙個比較有意思的故事 約瑟夫是猶太軍隊的乙個將軍,在反抗羅馬的起義中,他所率領的軍隊被擊潰,只剩下殘餘的部隊40餘人,他們都是寧死不屈的人,所以不願投降做叛徒。一群人表決說要死,所以用一種策略來先後殺死所有人。於是約瑟夫建議 每次由其他兩人一起殺死乙個人,而被殺的人的先後順序是由抽籤決定的,約瑟夫...