演算法競賽入門經典(第二版)-劉汝佳-第八章 高效演算法設計 習題(18/28)
說明
本文是我對第8章28道習題的練習總結,建議配合紫書——《演算法競賽入門經典(第2版)》閱讀本文。
另外為了方便做題,我在VOJ上開了一個contest,歡迎一起在上面做:第八章習題contest
如果想直接看某道題,請點開目錄後點開相應的題目!!!
習題
習8-1 UVA 1149 裝箱
題意
給定N(N≤10^5)個物品的重量Li,揹包的容量M,同時要求每個揹包最多裝兩個物品。求至少要多少個揹包才能裝下所有的物品。
思路
先對物品從小到大排序,然後從前往後貪心法搜尋求解。初始化i=0, j=n-1,[i, j)區間表示未放入揹包的物品,在(i, j)區間中搜索第一個大於M-a[i]的數k,則其左側第一個數(也可能這個數就是第i個數)應該和第i個數放入同一個揹包。[i, j)區間中,k和k後面的數應當放入單獨的揹包,因為剩下的物品中最輕的加上他們的各自重量都會超過容量M。
需要細心考慮多種情況。這裡再提供兩個測試用例:
INPUT
2
4 10
1 4 8 10
5 10
1 4 4 9 10
OUTPUT
3
3
程式碼
#include <cstdio>
#include <cstring>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100001;
int n, l;
int a[N];
int main(void)
{
int kase;
scanf("%d", &kase);
for (int t = 1; t <= kase; t++) {
scanf("%d%d", &n, &l);
for (int i = 0; i < n; i++)
scanf("%d", &a[i]);
sort(a, a+n);
int res = 0;
int i = 0, j = n;
while (i < j) {
int k = upper_bound(a+i+1 , a+j, l-a[i]) - a;
if (k > i+1) k--;
res += (j > k) ? j-k : 1;
//printf("i=%d, j=%d, k=%d, res=%d\n", i, j, k, res);
i++;
j = k;
}
if (t > 1) printf("\n");
printf("%d\n", res);
}
return 0;
}
習8-2 UVA 1610 聚會遊戲
題意
輸入一個n(2≤n≤1000,n是偶數)個字串的集合D,找一個長度最短的字串(不一定在D中出現)S,使得D中恰好一半串小於等於S,另一半串大於S。如果有多解,輸出字典序最小的解。例如,對於{JOSEPHINE, JERRY},輸出JF;對於{FRED, FREDDIE},輸出FRED。提示:本題看似簡單,實際上暗藏陷阱,需要考慮細緻、周全。
思路
思路很簡單,對字串排序後找到最中間的兩個字串a和b,然後找到大於等於a且小於b的最短字串中的字典序最小解。
但果然藏了非常多的陷阱,我一共WA了3發,最後一發還是粗心了,看了半天沒看出來,參考了別人的部落格才找到的錯誤。
這裡提供一組測試資料吧,基本上能過這組資料的這個題應該就能AC了。
INPUT
2
F
EG
2
FH
EG
2
F
EZ
2
F
EZZE
2
F
EZZEFF
0
OUTPUT
EG
F
EZ
EZZE
EZZF
另外本題還有另一種思路,能夠免除if else分析的麻煩。這樣寫的程式碼我估計一遍能夠AC。
由於不能馬上就直接得到答案,就一個一個字母去嘗試。這樣子就有點類似dfs了,假設a和b在第k位開始不同,先判斷在這裡填一個字母能否得出答案。這裡需注意:不能填了一個字母后就立馬搜尋下一個情況,因為題目首先要求是最短,所以要在不填下一個字母的情況下,把這個位置可能的字母都填上試試。發現必須要再填下一個字母時,才開始填寫下一個字母。
程式碼可參考這篇博文。
程式碼
#include <cstdio>
#include <cstring>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1001;
int n;
string s[N];
int main(void)
{
while (scanf("%d", &n) && n) {
for (int i = 0; i < n; i++)
cin >> s[i];
sort(s, s+n);
string a = s[n/2-1], b = s[n/2];
int k;
for (k = 0; k < min(a.size(), b.size()); k++)
if (a[k] != b[k]) break;
string res = a.substr(0, k);
if (k < a.size()) {
if (k+1 == a.size()) res += a[k];
else if (k+1 == b.size() && b[k] - a[k] == 1) {
res += a[k];
for (int i = k+1; i < a.size(); i++) {
if(i == a.size()-1) {
res += a[i]; break;
} else if (a[i] != 'Z') {
res += (a[i]+1); break;
}
res += 'Z';
}
} else res += (char)(a[k]+1);
}
cout << res << endl;
}
return 0;
}
習8-3 UVA 12545 位元變換器
題意
輸入兩個等長(長度不超過100)的串S和T,其中S包含字元0, 1, ?,但T只包含0和1。你的任務是用盡量少的步數把S變成T。每步有3種操作:把S中的0變成1;把S中的“?”變成0或者1;交換S中任意兩個字元。例如,01??00經過3步可以變成001010(方法是先把兩個問號變成1和0,再交換兩個字元)。
思路
貪心法求解。
由於只需要對不同的位置進行處理,先對兩個字串的不同位置進行分類統計,一共四種情況:
- S中為1,T中為0
- S中為0,T中為1
- S中為?,T中為0
- S中為?,T中為1
情況1不能由變換直接得到,需要與其他情況的位置交換得到,只能與情況2或4交換。與情況2交換隻耗費1個操作,與情況4交換耗費2個操作(先將情況4的位置的?換成0,再交換)。如果情況1與其他情況交換後還剩餘,則無解。情況1處理完後剩下的其他情況均只耗費1個操作。
程式碼
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
char s[101], t[101];
int main(void)
{
int n;
scanf("%d", &n);
for (int k = 1; k <= n; k++) {
scanf("%s%s", s, t);
int type[4];
memset(type, 0, sizeof(type));
for (int i = 0; s[i]; i++) {
if (s[i] != t[i]) {
if (s[i] == '1') type[0]++;
else if (s[i] == '0') type[1]++;
else if (t[i] == '0') type[2]++;
else type[3]++;
}
}
int res = min(type[0], type[1]);
type[0] -= res; type[1] -= res;
if (type[0]) {
int add = min(type[0], type[3]);
type[0] -= add; type[3] -= add;
if (type[0]) res = -1;
else res += 2*add + type[3] + type[2];
} else {
res += type[1] + type[2] + type[3];
}
printf("Case %d: %d\n", k, res);
}
return 0;
}
習8-4 UVA 11491 獎品的價值
題意
你是一個電視節目的獲獎嘉賓。主持人在黑板上寫出一個n位整數(不以0開頭),邀請你刪除其中的d個數字,剩下的整數便是你所得到的獎品的價值。當然,你希望這個獎品價值儘量大。1≤d<n≤10^5。
思路
一開始真的沒想到這竟然是一道貪心題目。看了別人的部落格才恍然大悟。
我採取的做法是自前向後掃一遍,用vector儲存選中的數,當前掃描的數s[i]大於vector尾部的數,那麼從它開始將它及其它之前的比s[i]小的數全部刪除。同時注意vector中數的個數加上剩下待掃描的數不能低於最終可選數n-d, 防止刪除多了。
程式碼
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 100001;
int n, d;
char s[N];
int main(void)
{
while (scanf("%d%d", &n, &d), n || d) {
scanf("%s", s);
vector<char> res;
for (int i = 0; s[i]; i++) {
int len;
while (len = res.size()) {
if (res[len-1] < s[i] && len+n-i > n-d)
res.resize(len-1);
else
break;
}
res.push_back(s[i]);
}
for (int i = 0; i < n-d; i++)
printf("%c", res[i]);
printf("\n");
}
return 0;
}
習8-5 UVA 177 摺痕
題意
思路
程式碼
習8-6 UVA 1611 起重機
題意
輸入一個1~n(1≤n≤10000)的排列,用不超過96次操作把它變成升序。每次操作都可以選一個長度為偶數的連續區間,交換前一半和後一半。例如,輸入5, 4, 6, 3, 2, 1,可以執行1, 2先變成4, 5, 6, 3, 2, 1,然後執行4, 5變成4, 5, 6, 2, 3, 1,然後執行5, 6變成4, 5, 6, 2, 1, 3,然後執行4, 5變成4, 5, 6, 1, 2, 3,最後執行操作1,6即可。
提示:2n次操作就足夠了。
思路
順序將1-n移到自己的位置上即可。將i移動到位置i時,先搜尋i目前所在位置j,如果j超出了i與n的中間位置,說明一次操作不能到位,需要兩次操作。所以總共最多需要2n次操作。
程式碼
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 10001;
int n;
int a[N];
typedef pair<int, int> P;
vector<P> res;
void exchange(int l, int r)
{
res.push_back(P(l, r));
int half = (r-l+1)/2;
for (int i = l; i < l+half; i++)
swap(a[i], a[i+half]);
/*
printf("=====exchange %d %d\n", l, r);
for (int i = 1; i <= n; i++)
printf("%d ", a[i]);
printf("\n");
*/
}
int main(void)
{
int kase;
scanf("%d", &kase);
while (kase--) {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
res.clear();
for (int i = 1; i <= n; i++) {
int j;
for (j = i; j <= n; j++)
if (a[j] == i) break;
if (i == j) continue;
int half = j-i;
if (2*half <= n-i+1) exchange(i, 2*half+i-1);
else {
half = (n-i+1)/2;
exchange(n-2*half+1, n);
exchange(i, 2*(j-half-i)+i-1);
}
}
printf("%d\n", res.size());
for (int i = 0; i < res.size(); i++)
printf("%d %d\n", res[i].first, res[i].second);
}
return 0;
}
習8-7 UVA 11925 生成排列
題意
輸入一個1~n(1≤n≤300)的排列,用不超過2n^2次操作將一個1-n的升序序列程式設計它。操作只有兩種:交換前兩個元素(操作1);把第一個元素移動到最後(操作2)。
書中說的是講排列變成升序數列,事實上說反了。
思路
開始把問題想複雜了,自己考慮定義了一個廣義的兩個相鄰數之間距離(這個距離對於操作2是不變的),如果交換能使得總距離變小則執行操作1,否則執行操作2。而當總距離達到最小且第一個數與排列的第一個數相等時結束迴圈。結果提交後TLE了,研究之後發現有一些情況下程式會進入死迴圈,無法下降到最小距離。
本題比較好的思路是逆向思考,把給定序列變成有序,操作相應變化一下,最後逆序輸出操作。
至於排序的問題,把序列看成一個環,第二種操作相當改變了可交換元素的位置,然後就可以等效為氣泡排序啦。。。
但需要注意的一點是,是因為是環狀的,和氣泡排序有所區別,最大的元素在頭部的時候不能進行交換了,否則陷入死迴圈,最大的元素所在的位置相當與鏈狀時候的最後面的有序區,是不需要比較的。
不過我本來是想通過逆序數來判斷迴圈結束的,後來發現無法正確定義逆序數,因為這個排列是迴圈的。只好用比較土的方法——比較整個排列與結果相等——判斷迴圈結束。
程式碼
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <deque>
#include <vector>
using namespace std;
int n;
deque<int> dq;
void print_dq()
{
for (int i = 0; i < n; i++)
printf("%d ", dq[i]);
printf("\n");
}
int main (void)
{
while (scanf("%d", &n) && n)
{
dq.clear();
int tmp;
for (int i = 1; i <= n; i++) {
scanf("%d", &tmp);
dq.push_back(tmp);
}
vector<int> res;
while (true) {
bool flag = true;
for (int i = 0; i < n; i++) {
if (dq[i] != i+1) {flag = false; break;}
}
if (flag == true) break;
if (dq[0] < dq[1] || (dq[0] == n && dq[1] == 1)) {
res.push_back(2);
dq.push_front(dq.back()); dq.pop_back();
} else {
res.push_back(1);
swap(dq[0], dq[1]);
}
//print_dq();
}
for (int i = res.size()-1; i >= 0; i--)
printf("%d", res[i]);
printf("\n");
}
return 0;
}
習8-8 UVA 1612 猜名次
題意
有n(n≤16384)位選手參加程式設計比賽。比賽有3道題目,每個選手的每道題目都有一個評測之前的預得分(這個分數和選手提交程式的時間相關,提交得越早,預得分越大)。接下來是系統測試。如果某道題目未通過測試,則該題的實際得分為0分,否則得分等於預得分。得分相同的選手,ID小的排在前面。
問是否能給出所有3n個得分以及最後的實際名次。如果可能,輸出最後一名的最高可能得分。每個預得分均為小於1000的非負整數,最多保留兩位小數。
思路
貪心法求解。
要最高分,那第一名肯定要三道題都對。維護一個最高分和上一個人的ID號,接著判斷一下下一名的得分。
如果有得分相同的情況下,就判斷一下ID號。如果當前這個人的ID號比較大,就只需要更新ID就可以了。
如果沒有得分相同的或者得分相同ID號比上一個人小,就找得分最大的且小於上一個人的得分的值即可。
如果上面兩個條件都不滿足,就是無解了。
另外需要注意判斷小數相等不要直接用==號,要考慮浮點誤差。
程式碼
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
const int N = 16385;
const double EPS = 1e-5;
int n;
double a[N][3];
int id[N];
int main(void)
{
int kase = 0;
while (scanf("%d", &n) && n) {
for (int i = 1; i <= n; i++)
scanf("%lf%lf%lf", &a[i][0], &a[i][1], &a[i][2]);
for (int i = 1; i <= n; i++)
scanf("%d", &id[i]);
int mi;
double mscore, pos[8];
int i;
for (i = 1; i <= n; i++) {
int j = id[i];
if (i == 1) {
mi = i;
mscore = a[j][0]+a[j][1]+a[j][2];
continue;
}
int k;
for (k = 0; k < 8; k++)
pos[k] = (k&1)*a[j][0] + ((k>>1)&1)*a[j][1] + ((k>>2)&1)*a[j][2];
sort(pos, pos+8);
for (k = 7; k >= 0; k--) {
if (pos[k] < mscore-EPS || j > id[i-1] && fabs(pos[k]-mscore) < EPS)
break;
}
if (k < 0) break;
mscore = pos[k];
}
printf("Case %d: ", ++kase);
if (i <= n) printf("No solution\n");
else printf("%.2lf\n", mscore);
}
return 0;
}
習8-9 UVA 1613 K度圖的著色
題意
輸入一個n(3≤n≤9999)個點m條邊(2≤m≤100000)的連通圖,n保證為奇數。設k為最小的奇數,使得每個點的度數不超過k,你的任務是把圖中的結點塗上顏色1~k,使得相鄰結點的顏色不同。多解時輸出任意解。輸入保證有解。
思路
用貪心法做的。優先選擇可選顏色少的點進行染色。如果先選可選顏色多的,其它的可選顏色少的進一步受到限制,到最後可能出現無法染色的情況。
儘管這樣做能夠AC,但在理論上無法證明這種方法的正確性。
我能夠確定正確的做法是DFS+回溯,但這樣做估計是會超時的。
此題留待以後進一步探討。
程式碼
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 10000;
const int M = 100001;
int n, m, k;
vector<int> next[N];
int color[N], chosen[N];
int c[N];
void init()
{
for (int i = 1; i <= n; i++)
next[i].clear();
memset(color, 0, sizeof(color));
memset(chosen, 0, sizeof(chosen));
}
void get_k()
{
k = 0;
for (int i = 1; i <= n; i++)
k = max(k, (int)next[i].size());
if (k%2 == 0) k++;
}
int main(void)
{
int kase = 0;
while (scanf("%d%d", &n, &m) != EOF) {
init();
int a, b;
for (int i = 0; i < m; i++) {
scanf("%d%d", &a, &b);
next[a].push_back(b);
next[b].push_back(a);
}
get_k();
for (int i = 1; i <= n; i++) {
// 選擇當前可選顏色最少的點p
int p = 0, cnt = -1;
for (int j = 1; j <= n; j++) {
if (!color[j] && chosen[j] > cnt) {
p = j;
cnt = chosen[j];
}
}
// 統計p的鄰居已經選擇的顏色
memset(c, 0, sizeof(c));
for (int j = 0; j < next[p].size(); j++) {
c[color[next[p][j]]] = 1;
chosen[next[p][j]]++;
}
// 從未選擇的顏色中選擇顏色
int q;
for (q = 1; q <= k; q++)
if (!c[q]) break;
color[p] = q;
}
if (kase++) printf("\n");
printf("%d\n", k);
for (int i = 1; i <= n; i++)
printf("%d\n", color[i]);
}
return 0;
}
習8-10 UVA 1614 奇怪的股市
題意
輸入一個長度為n(n≤100000)的序列a,滿足1≤ai≤i,要求確定每個數的正負號,使得所有數的總和為0。例如a={1, 2, 3, 4},則設4個數的符號分別是1, -1, -1, 1即可(1-2-3+4=0),但如果a={1, 2, 3, 3},則無解(輸出No)。
思路
如果序列和為奇數顯然無解,而如果是偶數則一定有解。貪心法從大到小檢查序列中的數即可。
至於貪心的正確性:
證明一個結論吧,對於1≤ai≤i+1,一定可以表示出1~sum[i]中的任意一個數.
對於i=1顯然成立,
假設對於i=k結論成立,那麼對於i=k+1來說,只要證明sum[k]+i,1≤i≤ak+1可以湊出來就行了。
因為sum[k]+i≥k+1,且1≤ak+1≤k+1,所以可以先選一個ak+1,剩下的0≤sum[k]+i-ak+1≤sum[k]一定是可以有前面的數字湊出來的。
這就證明了貪心的正確性。
程式碼
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100001;
int n;
int a[N], c[N];
int main(void)
{
while (scanf("%d", &n) != EOF) {
int sum = 0;
for (int i = 0; i < n; i++) {
scanf("%d", &a[i]);
sum += a[i];
}
if (sum&1) { printf("No\n"); continue; }
sort(a, a+n);
sum /= 2;
int add = 0;
memset(c, 0, sizeof(c));
for (int i = n-1; i >= 0; i--) {
if (add + a[i] <= sum) { add += a[i]; c[i] = 1;}
if (add == sum) break;
}
printf("Yes\n");
for (int i = 0; i < n; i++)
printf("%d%c", c[i] ? 1 : -1, i == n-1 ? '\n' : ' ');
}
return 0;
}
習8-11 UVA 1615 高速公路
題意
給定平面上n(n≤10^5)個點和一個值D,要求在x軸上(0-L範圍內)選出儘量少的點,使得對於給定的每個點,都有一個選出的點離它的歐幾里德距離不超過D。
思路
對於每個點,求出x軸上(0-L範圍內)與該點距離不超過D的區間。這樣就將問題轉化為用最少的點覆蓋所有區間的問題。然後貪心法求解即可。
程式碼
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
const int N = 100001;
struct P {
double l, r;
};
int n;
double len, d;
P p[N];
bool cmp(const P &a, const P &b)
{
return a.l < b.l;
}
int solve()
{
sort(p, p+n, cmp);
int res = 0, i = 0;
double posL, posR;
while (i < n) {
posL = p[i].l;
posR = p[i].r;
i++;
while ( i < n && p[i].l <= posR ) {
posL = p[i].l;
posR = (p[i].r < posR) ? p[i].r : posR;
i++;
}
res++;
}
return res;
}
int main(void)
{
while (scanf("%lf%lf%d", &len, &d, &n) != EOF) {
int flag = 1;
double x, y;
for (int i = 0; i < n; i++) {
cin >> x >> y;
double z = d*d - y*y;
if (z < 0) flag = 0;
z = sqrt(z);
p[i].l = max(x - z, 0.0);
p[i].r = min(x + z, len);
}
printf("%d\n", solve());
}
return 0;
}
習8-12 UVA 1153 顧客是上帝
題意
思路
程式碼
習8-13 UVA 10570 外星人聚會
題意
輸入1~n的一個排列(3≤n≤500),每次可以交換兩個整數。用最少的交換次數把排列變成1~n的一個環狀排列。
思路
我的做法是暴力搜尋+貪心選擇。
將原排列分別迴圈左移0~(n-1)個位置,對移動後的排列貪心的將其變成1-n的升序排列,另外還有貪心的將其變成n-1的降序排列,分別統計最小移動次數,從而得到總的最小移動次數。
程式碼
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 501;
int n;
int a0[N], a[N], c0[N], c[N];
int main(void)
{
while (scanf("%d", &n) && n) {
for (int i = 1; i <= n; i++)
scanf("%d", &a0[i]);
int res = n;
for (int i = 0; i < n; i++) {
for (int r = 0; r < 2; r++) {
for (int j = 1; j <= n; j++) {
a[j] = a0[(j+i-1)%n+1];
c[a[j]] = j;
}
int cnt = 0;
for (int j = 1; j <= n; j++) {
if (r == 0) {
if (a[j] != j) {
int k = a[j];
swap(a[j], a[c[j]]);
swap(c[k], c[j]);
cnt++;
}
} else {
int jj = n-j+1;
if (a[j] != jj) {
int k = a[j];
swap(a[j], a[c[jj]]);
swap(c[k], c[jj]);
cnt++;
}
}
}
res = min(res, cnt);
}
}
printf("%d\n", res);
}
return 0;
}
習8-14 UVA 1616 商隊搶劫者
題意
輸入n條線段,把每條線段變成原線段的一條子線段,使得改變之後所有線段等長且不相交(但是端點可以重合)。輸出最大長度(用分數表示)。例如,有3條線段[2,6],[1,4],[8,12],則最優方案是分別變成[3.5,6],[1,3.5],[8,10.5],輸出5/2。
書中描述漏說的一個重要前提:對於任意i和j,不會同時滿足 ai ≤ aj 且 bj ≤ bi。
思路
二分+貪心。
先用二分查詢來搜尋最大可行的長度。用貪心法來檢查是否滿足條件。設定迭代次數為100,足夠了。
然後找到最大可行長度以後,列舉分母的所有情況,因為分母範圍為1-n。
貪心法能夠使用的條件是:
書中描述漏說的一個重要前提:對於任意i和j,不會同時滿足 ai ≤ aj 且 bj ≤ bi。
該題的啟示是:
如果想不出來如何找到解,首先看看給出一個解,如何去驗證。
然後再想想如何找到解,比如使用二分查詢。
程式碼
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
const int N = 100001;
const int M = 1000000;
int n;
pair<int, int> a[N];
bool check(double mid)
{
double begin = 0;
for (int j = 0; j < n; j++) {
if(a[j].first > begin)
begin = a[j].first;
if(begin + mid > a[j].second)
return false;
begin = begin + mid;
}
return true;
}
int main()
{
while (scanf("%d", &n) != EOF) {
for(int i = 0; i < n; i++)
scanf("%d%d", &a[i].first, &a[i].second);
sort(a, a+n);
double lb = 0, ub = M, mid;
for(int i = 1; i <= 100; i++) {
mid = (lb+ub) / 2;
if(check(mid)) lb = mid;
else ub = mid;
}
int p = 0, q = 1;
for(int i = 1; i <= n; i++) {
int k = round(lb*i);
if(fabs((double)k/i - lb) < fabs((double)p/q - lb)) {
p = k; q = i;
}
}
printf("%d/%d\n", p, q);
}
return 0;
}
習8-15 UVA 1617 筆記本
題意
思路
程式碼
習8-16 UVA 1618 弱鍵
題意
思路
程式碼
習8-17 UVA 11536 最短子序列
題意
有n(n≤10^6)個0~m-1(m≤1000)的整陣列成一個序列。輸入k(k≤100),你的任務是找一個儘量短的連續子序列(xa, xa+1, xa+2,…, xb-1, xb),使得該子序列包含1~k的所有整數。
例如,n=20,m=12,k=4,序列為1 (2 3 7 1 12 9 11 9 6 3 7 5 4) 5 3 1 10 3 3,括號內部分是最優解。如果不存在滿足條件的連續子序列,輸出sequence nai。
思路
滑動視窗法。
滑動搜尋時需要有一個數組c統計1-k中每個數在當前視窗中出現的次數,另外有一個數num表示當前視窗中出現的1-k中不同數的個數。num的增減發生在某個數的統計次數從0變成1或相反時。
num=k時滿足條件,與res比較並更新最短子序列。
程式碼
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1000001;
const int K = 101;
int n, m, k;
int a[N], c[K];
int main()
{
int kase;
scanf("%d", &kase);
for (int t = 1; t <= kase; t++) {
scanf("%d%d%d", &n, &m, &k);
for (int i = 0; i < n; i++) {
if (i < 3) a[i] = i+1;
else a[i] = (a[i-1]+a[i-2]+a[i-3])%m + 1;
}
memset(c, 0, sizeof(c));
int num = 0, l = 0, r = 0, res = n+1;
while (true) {
while (r < n && num < k) {
if (a[r] <= k && c[a[r]]++ == 0) num++;
r++;
}
if (num < k) break;
res = min(res, r-l);
if (a[l] <= k && --c[a[l]] == 0) num--;
l++;
}
printf("Case %d: ", t);
if (res > n) printf("sequence nai\n");
else printf("%d\n", res);
}
return 0;
}
習8-18 UVA 1619 感覺不錯
題意
給出一個長度為n(n≤100000)的正整數序列ai,求出一段連續子序列al,…,ar, 使得(al+…+ar)*min{al,…,ar}儘量大。
思路
開始沒有思路,看了別人的部落格解析後豁然開朗。
既然所有數都是大於等於0的,那麼在一個區間最小值一定的情況下,這個區間越長越好(當然最小值為0時特殊)。那麼我們對每個a[i]都求出以它為最小值的周圍最大區間即可。
對一個數a[i],l[i]代表左邊第一個比它小的數的位置(不存在則為-1),r[i]代表右邊第一個比它小的位置(不存在則為n),則最大區間就是[l[i]+1, r[i]-1]。
如何構造l[i]呢?,從左往右構造一個單調遞增的棧(注意一定是單調的!)。當a[i]比棧頂元素小的時候,棧頂元素出棧,直到棧為空或當前棧頂元素小於a[i]時,這就找到了l[i]。同時將a[i]入棧繼續順序搜尋。
r[i]同理可求得。
最後求sum*min的最大值即可。這裡求sum用到一個技巧是求出所有sum(1~k)(k從1-n),這樣任意區間sum都可以用它來表示了。也就是說求任意區間的sum都是線性複雜度O(N)。詳見程式碼。
另外,注意陣列全零的特殊情況,如果寫出來WA的話,試一下這組資料:
6
0 0 0 0 0 0
結果應該是
0
1 1
而不是
0
1 6
程式碼
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<stack>
using namespace std;
const int N = 100001;
typedef long long LL;
int n;
int a[N];
LL sum[N];
int l[N], r[N];
stack<int> st;
int main()
{
int kase = 0;
while (scanf("%d", &n) != EOF) {
for (int i = 0; i < n; i++) {
scanf("%d", &a[i]);
sum[i] = (i == 0) ? a[0] : (sum[i-1]+a[i]);
}
// get l[N]
while (st.size()) st.pop();
for (int i = 0; i < n; i++) {
while (st.size() && a[st.top()] >= a[i]) st.pop();
l[i] = st.empty() ? -1 : st.top();
st.push(i);
}
// get r[N]
while (st.size()) st.pop();
for (int i = n-1; i >= 0; i--) {
while (st.size() && a[st.top()] >= a[i]) st.pop();
r[i] = st.empty() ? n : st.top();
st.push(i);
}
// get result
LL res = 0, cur;
int ll = -1, rr = 1;
for (int i = 0; i < n; i++) {
if (l[i] == -1) cur = sum[r[i]-1] * a[i];
else cur = (sum[r[i]-1]-sum[l[i]]) * a[i];
if (cur > res || cur == res && r[i]-l[i] < rr - ll) {
res = cur;
ll = l[i];
rr = r[i];
}
}
if (kase++) printf("\n");
printf("%lld\n%d %d\n", res, ll+2, rr);
}
return 0;
}
習8-19 UVA 1312 球場
題意
思路
程式碼