题目描述
使用代理服务器能够在一定程度上隐藏客户端信息,从而保护用户在互联网上的隐私。我们知道n个代理服务器的IP地址,现在要用它们去访问m个服务器。这 m 个服务器的 IP 地址和访问顺序也已经给出。系统在同一时刻只能使用一个代理服务器,并要求不能用代理服务器去访问和它 IP地址相同的服务器(不然客户端信息很有可能就会被泄露)。在这样的条件下,找到一种使用代理服务器的方案,使得代理服务器切换的次数尽可能得少。
输入描述:
- 每个测试数据包括 n + m + 2 行。
- 第 1 行只包含一个整数 n,表示代理服务器的个数。
- 第 2行至第n + 1行每行是一个字符串,表示代理服务器的 IP地址。这n个 IP地址两两不相同。
- 第 n + 2 行只包含一个整数 m,表示要访问的服务器的个数。
- 第 n + 3 行至第 n + m + 2 行每行是一个字符串,表示要访问的服务器的 IP 地址,按照访问的顺序给出。
- 每个字符串都是合法的IP地址,形式为“xxx.yyy.zzz.www”,其中任何一部分均是0–255之间的整数。输入数据的任何一行都不包含空格字符。
- 其中,1<=n<=1000,1<=m<=5000。
输出描述:
可能有多组测试数据,对于每组输入数据, 输出数据只有一行,包含一个整数s,表示按照要求访问服务器的过程中切换代理服务器的最少次数。第一次使用的代理服务器不计入切换次数中。若没有符合要求的安排方式,则输出-1。
正文
筆者在牛客網上兩次遇到這一題。第一次做這道題時,筆者通過自己的思考,給出了該題的一種比較新穎的解答;第二次做這道題時,筆者卻完全忘記了第一次的解法。而且時隔數月,筆者經過學習,了解到貪心策略這一概念,於是給出了第二種解答。翻看答題記錄,發現兩次的思路迥然不同。要想讀懂過去的代碼,理解其中的思想,還頗費了一番功夫。
解法一
顯然,若當前代理與目標服務器的地址相同,這時候就需要切換服務器。其它情況下,沒有更換服務器的必要。
切換代理服務器的次數稱作“代價”。每次切換代理服務器,代價加一。如果知道某一時刻使用各個代理服務器所需的代價,就可以求得下一時刻使用某代理服務器所需的最小代價。
此處舉一個例子:
代理分別是a, b, c. 待訪問的服務器依次是a, b, c, a, b, c. 可以列表如下
a | b | c | |
---|---|---|---|
a | 0 | 0 | |
b | 1 | 0 | |
c | 1 | 1 | |
a | 1 | 2 | |
b | 2 | 2 | |
c | 2 | 3 |
表格表示的是用各個代理服務器依次訪問各個服務器所需要的最小代價。
第一個要訪問的服務器是a。用代理服務器a來訪問服務器a是不允許的,因此對應的代價是。用代理服務器b、c來訪問a是合法的,記為0,這是因為這是第一個訪問對象,沒有必要切換服務器。於是第一行為
,0,0.
第二個要訪問的服務器是b。因為第一個代理服務器不能是a,所以要用a來訪問第二個服務器b的話,必須經過一次切換——可能是從b或者c切過來——不論如何代價都是0 + 1 = 1. 此時不可能通過代理b來訪問b,故對應的代價記為。通過c來訪問b的代價為0,這是因為用c訪問第一個服務器a的代價為0,同時c也能訪問第二個服務器b。於是第二行為1,
,0.
總之,若上一行的對應列不是,則下一行的對應列的數值直接繼承自上一行。否則,必須切換代理服務器,此時代價的數值是上一行的最小代價加一。
根據上一行的數值可以求下一行的數值,而我們所需要的只是最後一行,因此不必儲存整個表格,只要儲存兩行即可。
最後一行的數值計算完畢時,取最後一行的最小代價,就得到所求的最少切換次數。對於這個例子,從表格可以看出最少需要切換兩次代理服務器。
如果最小代價為,則表示沒有合法方案。
#include <iostream>
#include <algorithm>
int main() {
using namespace std;
int n, m;
while(cin >> n) {
string* arr_agent = new string[n];
for (int i = 0; i < n; i++) {
cin >> arr_agent[i];
}
cin >> m;
string* arr_server = new string[m];
for (int i = 0; i < m; i++) {
cin >> arr_server[i];
}
int* cost = new int[n];
int* cost2 = new int[n];
for (int i = 0; i < m; i++) {
string ip_server = arr_server[i];
int cost_min;
if (i > 0) {
cost_min = m;
for (int j = 0; j < n; j++) {
cost_min = std::min(cost_min, cost[j]);
}
}
else
cost_min = 0;
for (int j = 0; j < n; j++) {
string ip_agent = arr_agent[j];
if (ip_server == ip_agent) {
cost2[j] = m;
}
else
cost2[j] = std::min(cost[j], cost_min + 1);
}
int* temp = cost;
cost = cost2;
cost2 = temp;
}
int cost_min = m;
for (int j = 0; j < n; j++) {
cost_min = std::min(cost_min, cost[j]);
}
cost_min = cost_min < m ? cost_min : -1;
cout << cost_min << endl;
delete[] cost;
delete[] cost2;
delete[] arr_agent;
delete[] arr_server;
}
}
注意代碼中用m代替,這是因為代理切換次數不可能達到m次。
解法二
經過一段時間的學習,筆者了解到“貪心策略”這一概念,此時再看同一道題,筆者的思路也發生改變。
所謂貪心策略,即是說不考慮之後的情況,僅僅考慮局部最優解的方法。若一系列局部最優解恰恰組合成全局最優解,則貪心策略對於該問題是有效的。該題可以應用貪心策略,分析如下:
下面若說“代理x可以用n次”,即是說連續n個待訪問服務器都不與代理相衝突,但第n + 1個服務器與代理衝突。
若存在可供切換的代理,
首先,在任何時刻,能不切換代理的話就不去切換。
【證明】情況一:當前代理並不與服務器發生衝突,且今後也不會衝突。此時切換代理顯然是不明智的。
情況二:當前代理可以使用n次。若保持代理服務器不變直到使用n次,則可以在衝突前任意切換到其它代理,同時代價加一;若提前切換代理,則代價立即加一,但是使用n次前可能發生衝突,這樣代價的增量大於等於一。
綜合情況一和情況二,提前切換代理是不明智的。
其次,一旦需要切換代理,則使用可使用次數最多的代理。
【證明】在某個時刻需要切換代理,有代理a,b可以選擇。a可以用x次,b可以用y次且x < y. 若選擇代理a,則使用y + 1次之後,代價的增量大於等於一;若選擇代理b,則訪問使用y + 1次之後,代價的增量等於一,此時代理b可以任意切換為其它代理。因此代理b是更好的選擇。
同樣的,對於有多個代理可供選擇的情況,應當選擇可使用次數最多的代理。
根據上面兩條原則,可以設計出如下程序:
#include <iostream>
#include <vector>
#include <set>
using namespace std;
int main() {
int n, m;
while (cin >> n) {
vector<string> agents(n);
for (int i = 0; i < n; ++i) {
cin >> agents[i];
}
cin >> m;
vector<string> servers(m);
for (int i = 0; i < m; ++i)
cin >> servers[i];
int count = -1;
for (int pos = 0; pos < servers.size(); ) {
int max_pos = 0;
// 寻找当前条件下,用哪个代理可以访问最多服务器
for (int i = 0; i < agents.size(); ++i) {
string agent = agents[i];
int j;
for (j = pos; j < servers.size() && servers[j] != agent; ++j);
if (j > max_pos)
max_pos = j;
}
if (max_pos == pos) { // 不论用哪个服务器,都无法继续访问了
count = -1;
break;
}
count++;
pos = max_pos;
}
cout << count << endl;
}
return 0;
}
總結
回顧以上兩種思路,顯然解法二的“貪心策略”更為優越,但解法一的思路也很有趣。
兩種解法都要注意“沒有合法方案的情況”。顯然,若代理服務器只有一個,且待訪問服務器中存在與唯一的代理相衝突的地址,則合法方案是不存在的。