這是道好題,雖然我沒在現場比,但是也寫了兩個多小時,還和其他dalao的題解對照了半天才調出錯來(
一、題目
題目鏈接
↓↓↓如果上面沒有csp賬號,暫時沒法看題目的話,請看下面↓↓↓(好家伙把這個粘下來就200多行了湊字數,還鍛煉Markdown(((
試題背景
動態主機配置協議(Dynamic Host Configuration Protocol, DHCP)是一種自動為網絡客戶端分配 IP 地址的網絡協議。當支持該協議的計算機剛剛接入網絡時,它可以啟動一個 DHCP 客戶端程序。后者可以通過一定的網絡報文交互,從 DHCP 服務器上獲得 IP 地址等網絡配置參數,從而能夠在用戶不干預的情況下,自動完成對計算機的網絡設置,方便用戶連接網絡。DHCP 協議的工作過程如下:
- 當 DHCP 協議啟動的時候,DHCP 客戶端向網絡中廣播發送 Discover 報文,請求 IP 地址配置;
- 當 DHCP 服務器收到 Discover 報文時,DHCP 服務器根據報文中的參數選擇一個尚未分配的 IP 地址,分配給該客戶端。DHCP 服務器用 Offer 報文將這個信息傳達給客戶端;
- 客戶端收集收到的 Offer 報文。由於網絡中可能存在多於一個 DHCP 服務器,因此客戶端可能收集到多個 Offer 報文。客戶端從這些報文中選擇一個,並向網絡中廣播 Request 報文,表示選擇這個 DHCP 服務器發送的配置;
- DHCP 服務器收到 Request 報文后,首先判斷該客戶端是否選擇本服務器分配的地址:如果不是,則在本服務器上解除對那個 IP 地址的占用;否則則再次確認分配的地址有效,並向客戶端發送 Ack 報文,表示確認配置有效,Ack 報文中包括配置的有效時間。如果 DHCP 發現分配的地址無效,則返回 Nak 報文;
- 客戶端收到 Ack 報文后,確認服務器分配的地址有效,即確認服務器分配的地址未被其它客戶端占用,則完成網絡配置,同時記錄配置的有效時間,出於簡化的目的,我們不考慮被占用的情況。若客戶端收到 Nak 報文,則從步驟 1 重新開始;
- 客戶端在到達配置的有效時間前,再次向 DHCP 服務器發送 Request 報文,表示希望延長 IP 地址的有效期。DHCP 服務器按照步驟 4 確定是否延長,客戶端按照步驟 5 處理后續的配置;
在本題目中,你需要理解 DHCP 協議的工作過程,並按照題目的要求實現一個簡單的 DHCP 服務器。
問題描述
報文格式
為了便於實現,我們簡化地規定 DHCP 數據報文的格式如下:
<發送主機> <接收主機> <報文類型> <IP 地址> <過期時刻>
DHCP 數據報文的各個部分由空格分隔,其各個部分的定義如下:
- 發送主機:是發送報文的主機名,主機名是由小寫字母、數字組成的字符串,唯一地表示了一個主機;
- 接收主機:當有特定的接收主機時,是接收報文的主機名;當沒有特定的接收主機時,為一個星號(*);
- 報文類型:是三個大寫字母,取值如下:
- DIS:表示 Discover 報文;
- OFR:表示 Offer 報文;
- REQ:表示 Request 報文;
- ACK:表示 Ack 報文;
- NAK:表示 Nak 報文;
- IP 地址,是一個非負整數:
- 對於 Discover 報文,該部分在發送的時候為 0,在接收的時候忽略;
- 對於其它報文,為正整數,表示一個 IP 地址;
- 過期時刻,是一個非負整數:
- 對於 Offer、Ack 報文,是一個正整數,表示服務器授予客戶端的 IP 地址的過期時刻;
- 對於 Discover、Request 報文,若為正整數,表示客戶端期望服務器授予的過期時刻;
- 對於其它報文,該部分在發送的時候為 0,在接收的時候忽略。
例如下列都是合法的 DHCP 數據報文:
a * DIS 0 0
d a ACK 50 1000
服務器配置
為了 DHCP 服務器能夠正確分配 IP 地址,DHCP 需要接受如下配置:
- 地址池大小 \(N\):表示能夠分配給客戶端的 IP 地址的數目,且能分配的 IP 地址是 ;
- 默認過期時間 \(T_{def}\):表示分配給客戶端的 IP 地址的默認的過期時間長度;
- 過期時間的上限和下限 \(T_{max}\)、\(T_{min}\):表示分配給客戶端的 IP 地址的最長過期時間長度和最短過期時間長度,客戶端不能請求比這個更長或更短的過期時間;
- 本機名稱 \(H\):表示運行 DHCP 服務器的主機名。
分配策略
當客戶端請求 IP 地址時,首先檢查此前是否給該客戶端分配過 IP 地址,且該 IP 地址在此后沒有被分配給其它客戶端。如果是這樣的情況,則直接將 IP 地址分配給它,否則,
總是分配給它最小的尚未占用過的那個 IP 地址。如果這樣的地址不存在,則分配給它最小的此時未被占用的那個 IP 地址。如果這樣的地址也不存在,說明地址池已經分配完畢,因此拒絕分配地址。
實現細節
在 DHCP 啟動時,首先初始化 IP 地址池,將所有地址設置狀態為未分配,占用者為空,並清零過期時刻。
其中地址的狀態有未分配、待分配、占用、過期四種。
處於未分配狀態的 IP 地址沒有占用者,而其余三種狀態的 IP 地址均有一名占用者。
處於待分配和占用狀態的 IP 地址擁有一個大於零的過期時刻。在到達該過期時刻時,若該地址的狀態是待分配,則該地址的狀態會自動變為未分配,且占用者清空,過期時刻清零;否則該地址的狀態會由占用自動變為過期,且過期時刻清零。處於未分配和過期狀態的 IP 地址過期時刻為零,即沒有過期時刻。
對於收到的報文,設其收到的時刻為 \(t\)。處理細節如下:
- 判斷接收主機是否為本機,或者為 *,若不是,則判斷類型是否為 Request,若不是,則不處理;
- 若類型不是 Discover、Request 之一,則不處理;
- 若接收主機為 *,但類型不是 Discover,或接收主機是本機,但類型是 Discover,則不處理。
對於 Discover 報文,按照下述方法處理:
- 檢查是否有占用者為發送主機的 IP 地址:
-
- 若有,則選取該 IP 地址;
- 若沒有,則選取最小的狀態為未分配的 IP 地址;
- 若沒有,則選取最小的狀態為過期的 IP 地址;
- 若沒有,則不處理該報文,處理結束;
-
將該 IP 地址狀態設置為待分配,占用者設置為發送主機;
-
若報文中過期時刻為 0 ,則設置過期時刻為 \(t+T_{def}\);否則根據報文中的過期時刻和收到報文的時刻計算過期時間,判斷是否超過上下限:若沒有超過,則設置過期時刻為報文中的過期時刻;否則則根據超限情況設置為允許的最早或最晚的過期時刻;
-
向發送主機發送 Offer 報文,其中,IP 地址為選定的 IP 地址,過期時刻為所設定的過期時刻。
對於 Request 報文,按照下述方法處理: -
檢查接收主機是否為本機:
-
- 若不是,則找到占用者為發送主機的所有 IP 地址,對於其中狀態為待分配的,將其狀態設置為未分配,並清空其占用者,清零其過期時刻,處理結束;
- 檢查報文中的 IP 地址是否在地址池內,且其占用者為發送主機,若不是,則向發送主機發送 Nak 報文,處理結束;
- 無論該 IP 地址的狀態為何,將該 IP 地址的狀態設置為占用;
- 與 Discover 報文相同的方法,設置 IP 地址的過期時刻;
- 向發送主機發送 Ack 報文。
上述處理過程中,地址池中地址的狀態的變化可以概括為如下圖所示的狀態轉移圖。為了簡潔,該圖中沒有涵蓋需要回復 Nak 報文的情況。
輸入格式
輸入的第一行包含用空格分隔的四個正整數和一個字符串,分別是:\(N\)、\(T_{def}\)、\(T_{max}\)、\(T_{min}\) 和 \(H\),保證 。
輸入的第二行是一個正整數 \(n\),表示收到了 \(n\) 個報文。
輸入接下來有 \(n\) 行,第 \((i+2)\) 行有空格分隔的正整數 \(t_i\) 和約定格式的報文 \(P_i\) 。表示收到的第 \(i\) 個報文是在 \(t_i\) 時刻收到的,報文內容是 \(P_i\)。保證 \(t_i<t_{i+1}\)。
輸出格式
輸出有若干行,每行是一個約定格式的報文。依次輸出 DHCP 服務器發送的報文。
樣例輸入
4 5 10 5 dhcp
16
1 a * DIS 0 0
2 a dhcp REQ 1 0
3 b a DIS 0 0
4 b * DIS 3 0
5 b * REQ 2 12
6 b dhcp REQ 2 12
7 c * DIS 0 11
8 c dhcp REQ 3 11
9 d * DIS 0 0
10 d dhcp REQ 4 20
11 a dhcp REQ 1 20
12 c dhcp REQ 3 20
13 e * DIS 0 0
14 e dhcp REQ 2 0
15 b dhcp REQ 2 25
16 b * DIS 0 0
樣例輸出
dhcp a OFR 1 6
dhcp a ACK 1 7
dhcp b OFR 2 9
dhcp b ACK 2 12
dhcp c OFR 3 12
dhcp c ACK 3 13
dhcp d OFR 4 14
dhcp d ACK 4 20
dhcp a ACK 1 20
dhcp c ACK 3 20
dhcp e OFR 2 18
dhcp e ACK 2 19
dhcp b NAK 2 0
樣例說明
輸入第一行,分別設置了 DHCP 的相關參數,並收到了 16 個報文。
第 1 個報文和第 2 個報文是客戶端 a 正常請求地址,服務器為其分配了地址 1,相應地設置了過期時刻是 7(即當前時刻 2 加上默認過期時間 5)。
第 3 個報文不符合 Discover 報文的要求,不做任何處理。
第 4 個報文 b 發送的 Discover 報文雖然有 IP 地址 3,但是按照處理規則,這個字段被忽略,因此服務器返回 Offer 報文,過期時刻是 9。
第 5 個報文中,Request 報文不符合接收主機是 DHCP 服務器本機的要求,因此不做任何處理。
第 6 個報文是 b 發送的 Request 報文,其中設置了過期時刻是 12,沒有超過最長過期時間,因此返回的 Ack 報文中過期時刻也是 12。
第 7 個報文中,過期時刻 11 小於最短過期時間,因此返回的過期時刻是 12。雖然此時為 a 分配的地址 1 過期,但是由於還有狀態為未分配的地址 3,因此為 c 分配地址 3。第 8 個報文同理,為 c 分配的地址過期時刻是 13。
第 9、10 兩個報文中,為 d 分配了地址 4,過期時刻是 20。
第 11 個報文中,a 請求重新獲取此前為其分配的地址 1,雖然為其分配的地址過期,但是由於尚未分配給其它客戶端,因此 DHCP 服務器可以直接為其重新分配該地址,並重新設置過期時刻為 20。
第 12 個報文中,c 請求延長其地址的過期時刻為 20。DHCP 正常向其回復 Ack 報文。
第 13、14 個報文中,e 試圖請求地址。此時地址池中已經沒有處於“未分配”狀態的地址了,但是有此前分配給 b 的地址 2 的狀態是“過期”,因此把該地址重新分配給 e。
第 15 個報文中,b 試圖重新獲取此前為其分配的地址 2,但是此時該地址已經被分配給 e,因此返回 Nak 報文。
第 16 個報文中,b 試圖重新請求分配一個 IP 地址,但是此時地址池中已經沒有可用的地址了,因此忽略該請求。
樣例輸入
4 70 100 50 dhcp
6
5 a * OFR 2 100
10 b * DIS 0 70
15 b dhcp2 REQ 4 60
20 c * DIS 0 70
70 d * DIS 0 120
75 d dhcp REQ 1 125
樣例輸出
dhcp b OFR 1 70
dhcp c OFR 1 70
dhcp d OFR 1 120
dhcp d ACK 1 125
樣例說明
在本樣例中,DHCP 服務器一共收到了 6 個報文,處理情況如下:
第 1 個報文不是 DHCP 服務器需要處理的報文,因此不回復任何報文。
第 2 個報文中,b 請求分配 IP 地址,因此 DHCP 服務器將地址 1 分配給 b,此時,地址 1 進入待分配狀態,DHCP 服務器向 b 發送 Offer 報文。
第 3 個報文中,b 發送的 REQ 報文是發給非本服務器的,因此需要將地址池中所有擁有者是 b 的待分配狀態的地址修改為未分配。
第 4 個報文中,c 請求分配 IP 地址。由於地址 1 此時是未分配狀態,因此將該地址分配給它,向它發送 Offer 報文,地址 1 進入待分配狀態。
第 5、6 個報文中,d 請求分配 IP 地址。注意到在收到第 5 個報文時,已經是時刻 70,地址 1 的過期時刻已到,它的狀態已經被修改為了未分配,因此 DHCP 服務器仍然將地址 1 分配給 d。
評測用例規模與約定
對於 20% 的數據,有 \(N\leq 200\),且 \(n\leq N\),且輸入僅含 Discover 報文,且 \(t<T_{min}\);
對於 50% 的數據,有 \(N\leq 200\),且 \(n\leq N\),且 \(t<T_{min}\),且報文的接收主機或為本機,或為 *;
對於 70% 的數據,有 \(N \leq 1000\),且 \(n\leq N\),且報文的接收主機或為本機,或為 *;
對於 100% 的數據,有 \(N\leq 10000\),且 \(n\leq 10000\),主機名的長度不超過 20,且 \(t,T_{min},T_{def},T_{max}\leq 10^9\),輸入的報文格式符合題目要求,且數字不超過 \(10^9\)。
二、思路
估計大家一看到這個題目就嚇死了。這么長,一定不簡單吧?但是,你只要認真看了一遍題目,就會發現你其實只需要參考實現細節把代碼寫一遍就好了,所以這個模擬不難。
讓我們一步步的來。
-1.初始化
在 DHCP 啟動時,首先初始化 IP 地址池,將所有地址設置狀態為未分配,占用者為空,並清零過期時刻。
其中地址的狀態有未分配、待分配、占用、過期四種。
處於未分配狀態的 IP 地址沒有占用者,而其余三種狀態的 IP 地址均有一名占用者。
那很簡單,我們用結構體表示一下每個ip地址,存儲狀態、過期時間和占用者。
0.處理過期
處於待分配和占用狀態的 IP 地址擁有一個大於零的過期時刻。在到達該過期時刻時,若該地址的狀態是待分配,則該地址的狀態會自動變為未分配,且占用者清空,過期時刻清零;否則該地址的狀態會由占用自動變為過期,且過期時刻清零。處於未分配和過期狀態的 IP 地址過期時刻為零,即沒有過期時刻。
我們在每次輸入過后把時間跳躍到那個時間(數據保證了\(t_i<t_{i+1}\)),每次遍歷一次去找。
1.處理Discover報文
- 檢查是否有占用者為發送主機的 IP 地址:
-
- 若有,則選取該 IP 地址;
- 若沒有,則選取最小的狀態為未分配的 IP 地址;
- 若沒有,則選取最小的狀態為過期的 IP 地址;
- 若沒有,則不處理該報文,處理結束;
- 將該 IP 地址狀態設置為待分配,占用者設置為發送主機;
- 若報文中過期時刻為 0 ,則設置過期時刻為 \(t+T_{def}\);否則根據報文中的過期時刻和收到報文的時刻計算過期時間,判斷是否超過上下限:若沒有超過,則設置過期時刻為報文中的過期時刻;否則則根據超限情況設置為允許的最早或最晚的過期時刻;
- 向發送主機發送 Offer 報文,其中,IP 地址為選定的 IP 地址,過期時刻為所設定的過期時刻。
檢查是否有占用者為發送主機的IP地址,也是遍歷一遍,找到第一個就是了。
找未分配的和過期的,可以打包成一個函數,找指定的狀態,也是遍歷一遍。
第二步直接模擬即可。
第三步中的根據超限情況,只需要和最早時間選一個最大值,再和最晚時間選一個最小值。
第四步直接輸出。
2.處理Request報文
- 檢查接收主機是否為本機:
-
- 若不是,則找到占用者為發送主機的所有 IP 地址,對於其中狀態為待分配的,將其狀態設置為未分配,並清空其占用者,清零其過期時刻,處理結束;
- 檢查報文中的 IP 地址是否在地址池內,且其占用者為發送主機,若不是,則向發送主機發送 Nak 報文,處理結束;
- 無論該 IP 地址的狀態為何,將該 IP 地址的狀態設置為占用;
- 與 Discover 報文相同的方法,設置 IP 地址的過期時刻;
- 向發送主機發送 Ack 報文。
第一步直接遍歷一遍,修改每一個。
第2~3步,照着寫。
第四步,和Discover的第三步一樣。
第五步直接輸出。
三、代碼
#include<bits/stdc++.h>
using namespace std;
struct IP{
int sta;//0:未分配、1:待分配、2:占用、3:過期。
string zhan;
int gq;//過期
}a[10010];
int N,tdef,tmax,tmin,n,t;//地址池大小,一般延時,最長延時,最短延時
string h;//本機
string st,en,ty;//報文發送者、接受者和類型
int ip,ti,fp,gg;//IP地址和時間,分配的地址 ,計算過期時間
int finds(string s){//檢查是否有占用者為發送主機的 IP 地址
for(int i=1;i<=N;i++){
if(a[i].zhan==s) return i;
}
return 0;
}
int findn(int st){//選取最小的狀態為指定值的 IP 地址
for(int i=1;i<=N;i++){
if(a[i].sta==st) return i;
}
return 0;
}
void chuli(int t){//處理過期
for(int i=1;i<=N;i++){
if(a[i].gq&&a[i].gq<=t){//如果有過期時間,而且已經過期
if(a[i].sta==1) a[i].sta=0,a[i].zhan="";//設成未分配
else a[i].sta=3;//設成過期
a[i].gq=0;
}
}
}
int main(){
ios::sync_with_stdio(0);//防超時
cin>>N>>tdef>>tmax>>tmin>>h>>n;//輸入
while(n--){
cin>>t>>st>>en>>ty>>ip>>ti;
if(en!=h&&en!="*"&&ty!="REQ") continue;//如果這個是對別人的其他報文,那就與我們無瓜了
if(ty!="REQ"&&ty!="DIS") continue;//其他種類報文不需要我們處理
if(en=="*"&&ty!="DIS"||en==h&&ty=="DIS") continue;//如果是面向所有人的非Discover報文,或者是面向本機的Discover報文,均不符合格式
chuli(t);//處理過期
if(ty=="DIS"){//Discover報文
fp=finds(st);//找到發送者的ip地址
if(!fp) fp=findn(0);//找到未分配的ip地址
if(!fp) fp=findn(3);//找到過期的ip地址
if(!fp) continue;//都沒有,拒絕
a[fp].sta=1;//設置
a[fp].zhan=st;
if(ti==0) a[fp].gq=t+tdef;//直接選擇
else{
gg=ti-t;//選擇界限
gg=max(gg,tmin);
gg=min(gg,tmax);
a[fp].gq=t+gg;
}
cout<<h<<" "<<st<<" OFR "<<fp<<" "<<a[fp].gq<<endl;//offer報文
}else{
if(en!=h){//選擇了其他的服務器
for(int i=1;i<=N;i++){
if(a[i].zhan==st&&a[i].sta==1){
a[i].sta=0,a[i].zhan="",a[i].gq=0;//清空所有有關ip
}
}
continue;
}
if(ip>N||ip<1||a[ip].zhan!=st){//不存在的ip或者要別人地址
//cout<<ip<<":"<<a[ip].zhan<<endl;
cout<<h<<" "<<st<<" NAK "<<ip<<" "<<0<<endl;
}else{
a[ip].sta=2;//設為占用
a[ip].zhan=st;//占用者
if(ti==0) a[ip].gq=t+tdef;//和上面一樣
else{
gg=ti-t;
gg=max(gg,tmin);
gg=min(gg,tmax);
a[ip].gq=t+gg;
}
cout<<h<<" "<<st<<" ACK "<<ip<<" "<<a[ip].gq<<endl;//設置成功
}
}
}
return 0;
}
四、總結
這個其實也沒啥好講的,就是一個純粹的模擬。
代碼也不長,只有80多行。
只能算是一個練碼量的小模擬吧,算一道好題,推薦在沒事干想刷題找回感覺的時候做。
順便慶祝題目和代碼使得本文超過了300行(
五、彩蛋
抱歉,這周一直忘了這件事(((
彩蛋就是:
哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈