DHCP 服务器

按时刻处理租约到期事件,并模拟地址池状态与 DHCP 报文规则。

OJ: shumeng

题目 ID: CSP202104C

难度:提高+/省选-

标签:模拟优先队列状态机

日期: 2026-07-31 16:21

形式化题目

模拟一个 DHCP 服务器。地址池为 1N1\sim N,每个地址有未分配、待分配、占用、过期四种状态。给定若干条报文(Discover 或 Request),按规则更新地址状态并输出服务器回复的 Offer、Ack、Nak 报文。

思路

把地址的状态、占用者和过期时刻记录下来,并把"当前时刻推进"转化为处理每条报文前统一执行一次到期检查。

到期处理

处理每条报文前,弹出所有过期时刻不超过当前时刻的事件:待分配地址回到未分配,占用地址变为过期。用两个有序集合分别维护最小未分配地址与最小过期地址,用堆维护过期事件,用映射维护主机名到其地址。

报文处理

先按题意过滤接收者与类型,然后:

  • Discover:优先选择该主机原有地址,否则选最小未分配地址,再选最小过期地址,回复 Offer 并把地址置为待分配;
  • Request 发给其它服务器:撤销该主机所有待分配地址;
  • Request 发给本机:校验报文地址的占用者,合法则置为占用并回复 Ack,否则回复 Nak。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-31 16:21
 * update_at: 2026-08-17 22:39
 */
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 10005;

// 地址的四种状态
enum Status { FREE, PENDING, OCCUPIED, EXPIRED };

// 一个 IP 地址的记录:状态、过期时刻、占用者
struct Address {
    int status;
    long long expire;
    string owner;
};

// 租约过期事件,堆顶为过期时刻最小的事件
struct ExpireEvent {
    long long time;
    int ip;
    bool operator<(const ExpireEvent &other) const {
        return time > other.time;
    }
};

int address_count;
long long default_time, maximum_time, minimum_time;
string server_name;
Address address[MAXN];
set<int> free_address, expired_address;   // 最小未分配与最小过期地址集合
priority_queue<ExpireEvent> expire_queue; // 过期事件队列
map<string, int> owner_ip;                // 主机名 -> 它占用的 IP

// 把地址释放为未分配状态,并清理相关记录
void set_free(int ip) {
    expired_address.erase(ip);
    owner_ip.erase(address[ip].owner);
    address[ip].status = FREE;
    address[ip].expire = 0;
    address[ip].owner.clear();
    free_address.insert(ip);
}

// 处理时刻 now 之前所有到期的租约
void process_expire(long long now) {
    while (!expire_queue.empty() && expire_queue.top().time <= now) {
        ExpireEvent event = expire_queue.top();
        expire_queue.pop();
        if (address[event.ip].expire != event.time) continue;   // 已被覆盖的过期事件
        if (address[event.ip].status == PENDING) {
            set_free(event.ip);                                 // 待分配到期 -> 未分配
        } else if (address[event.ip].status == OCCUPIED) {
            address[event.ip].status = EXPIRED;                 // 占用到期 -> 过期
            address[event.ip].expire = 0;
            expired_address.insert(event.ip);
        }
    }
}

// 根据请求过期时刻和上下限确定最终过期时刻
long long get_expire(long long now, long long requested) {
    if (requested == 0) return now + default_time;
    if (requested - now < minimum_time) return now + minimum_time;
    if (requested - now > maximum_time) return now + maximum_time;
    return requested;
}

// 把地址设为待分配,登记占用者并安排过期事件
void occupy_pending(int ip, const string &owner, long long expire) {
    free_address.erase(ip);
    expired_address.erase(ip);
    if (address[ip].owner != owner) owner_ip.erase(address[ip].owner);
    address[ip].status = PENDING;
    address[ip].owner = owner;
    owner_ip[owner] = ip;
    address[ip].expire = expire;
    expire_queue.push({expire, ip});
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> address_count >> default_time >> maximum_time >> minimum_time >> server_name;
    for (int i = 1; i <= address_count; i++) free_address.insert(i);
    int message_count;
    cin >> message_count;
    while (message_count--) {
        long long now, requested_expire;
        string sender, receiver, type;
        int ip;
        cin >> now >> sender >> receiver >> type >> ip >> requested_expire;
        process_expire(now);

        // 按题意过滤报文:接收者与类型必须符合要求
        if (receiver != server_name && receiver != "*" && type != "REQ") continue;
        if (type != "DIS" && type != "REQ") continue;
        if ((receiver == "*" && type != "DIS") || (receiver == server_name && type == "DIS")) continue;

        if (type == "DIS") {
            // 选择顺序:原有地址 > 最小未分配 > 最小过期
            int selected = owner_ip.count(sender) ? owner_ip[sender] : 0;
            if (selected == 0 && !free_address.empty()) selected = *free_address.begin();
            if (selected == 0 && !expired_address.empty()) selected = *expired_address.begin();
            if (selected == 0) continue;   // 无可用地址,忽略报文
            long long expire = get_expire(now, requested_expire);
            occupy_pending(selected, sender, expire);
            cout << server_name << ' ' << sender << " OFR " << selected << ' ' << expire << '\n';
            continue;
        }

        // Request 发给其它服务器:撤销发送方所有待分配地址
        if (receiver != server_name) {
            int selected = owner_ip.count(sender) ? owner_ip[sender] : 0;
            if (selected && address[selected].status == PENDING) set_free(selected);
            continue;
        }
        // Request 发给本机:校验占用者并确认租约
        if (ip < 1 || ip > address_count || address[ip].owner != sender) {
            cout << server_name << ' ' << sender << " NAK " << ip << " 0\n";
            continue;
        }
        free_address.erase(ip);
        expired_address.erase(ip);
        long long expire = get_expire(now, requested_expire);
        address[ip].status = OCCUPIED;
        address[ip].expire = expire;
        expire_queue.push({expire, ip});
        cout << server_name << ' ' << sender << " ACK " << ip << ' ' << expire << '\n';
    }

    return 0;
}

复杂度

每个地址状态变化至多产生一个可验证的过期事件。除输出外每个报文的集合和堆操作均为 O(logN)O(\log N),空间复杂度为 O(N)O(N)

总结

把到期处理放在每个报文处理之前,能把逻辑时钟规则转为普通事件模拟;地址状态和最小可用地址集合必须同步更新。