下面我們通過解決HDU題庫中的幾道應用貪心法思想編寫程序的例題,進一步體會貪心法的應用。
【例1】卡片游戲。
問題描述
小明最近宅在家里無聊,於是他發明了一種有趣的游戲,游戲道具是N張疊在一起的卡片,每張卡片上都有一個數字,數字的范圍是0~9,游戲規則如下:
首先取最上方的卡片放到桌子上,然后每次取最上方的卡片,放到桌子上已有卡片序列的最右邊或者最左邊。當N張卡片全部都放到桌子上后,桌子上的N張卡片構成了一個數。這個數不能有前導0,也就是說最左邊的卡片上的數字不能是0。游戲的目標是使這個數最小。
現在你的任務是幫小明寫段程序,求出這個最小數。
輸入
第一行是一個數T(T<=1000),表示有T組測試數據;
然后下面有T行, 每行是一個只含有0~9的字符串,表示N(1 <= N <= 100)張疊在一起的卡片,最左邊的數字表示最上方的卡片。
輸出
對於每組測試數據,請在一行內輸出能得到的最小數。
輸入樣例
3
565
9876543210
9876105432
輸出樣例
556
1234567890
1678905432
(1)編程思路。
貪心的策略是:數字越小的越應該放在最前面。由於放在最前的數字不能是0,因此需要先遍歷整個字符串,找最靠后的最小的非0數字,記錄其下標位置pos,以保證其放在第一位。
由於游戲時,依次取出的每張卡片可以放在桌子上已有卡片序列的最右邊或者最左邊,因此可以定義一個字符串dest[210](長度為源串的2倍),然后從中間位置開始放數字,定義兩個變量left和right分別表示放每張卡片的左端或右端的位置,初始值均為101(中間位置),將第1個數src[0]放在最左端,即dest[--left]=src[0]。之后從src[1]開始遍歷源字符串,對於串中的每個字符src[i],進行如下處理:
1)若下標i小於pos,即還沒有訪問到最靠后的最小的非0數字。則與最左端的數字進行比較,大就放到最右端(后面),小就放到最左端(前面)。
2)若下標i等於pos,則將其放在最左端,保證最小的非0數字放在第一位。
3)下標大於pos的字符就只能依次放在最右端了。
(2)源程序。
#include <stdio.h>
int main()
{
int t;
scanf("%d",&t);
while (t--){
char src[101],dest[210];
int i;
scanf("%s",src);
char min='9';
int pos=101;
for (i=0;src[i]!='\0';i++){
if (src[i]!='0' && min>=src[i]){
min=src[i]; pos=i;
}
}
int left=101;
int right=101;
dest[--left]=src[0];
for (i=1;src[i]!='\0';i++){
if (i<pos){
if (src[i]>dest[left]){
dest[right++]=src[i];
}
else
dest[--left]=src[i];
}
else if (i==pos)
dest[--left]=src[i];
else
dest[right++]=src[i];
}
for (i=left;i<right;i++)
printf("%c",dest[i]);
printf("\n");
}
return 0;
}
注:將此源程序提交給HDU4550卡片游戲(http://acm.hdu.edu.cn/showproblem.php?pid=4550)可以Accepted。
【例2】去掉m個數字。
問題描述
給定一個最多1000位的整數n,從整數n中任意拿掉m個數字后,n最小為多少?
輸入
輸入包括多個測試用例。
每個測試用例將包含一個給定的整數(最多可能包含1000個數字)和整數m(如果整數包含n個數字,m將不大於n)。給定的整數不包含前導零。
輸出
對於每組測試數據,請在一行內輸出能得到的最小數,最小數的前導0需去掉。
輸入樣例
178543 4
1000001 1
100001 2
12345 2
54321 2
輸出樣例
13
1
0
123
321
(1)編程思路。
n位的整數去掉m位后會留下n-m位,因此貪心的策略是進行n-m次挑選,每次在剩下的可挑選的數字中找出最小的數字留下來即可。
設給定的整數保存在字符串s中,s[0]保存最高位,s[n-1]保存最低位。
第1個數字一定在會在s[0]~s[m]中選一個最小的,因為后面至少得留n-m-1個數字供后面進行選擇。若有多個最小的,則選取下標值最小的位置的數字,並標記其位置pos1。將所選取的最小的數字作為結果的第一位(最高位)進行保存。
第2個數字在s[pos1]~s[m+1]中進行選擇,同樣后面至少得留n-m-2個數字供以后進行選擇。在s[pos1]~s[m+1]中選擇最小的數字時,若有多個最小的,則選取下標值最小的位置的數字,並標記其位置pos2。將所選取的最小的數字作為結果的第2位進行保存。
依次類推,第3個數字在s[pos2]~s[m+2]中進行選擇,……,直到完成n-m次選擇。
(2)源程序。
#include <stdio.h>
#include <string.h>
int main()
{
char s[1005],ans[1005];
int ok[1005];
while(~scanf("%s",s)){
int len=strlen(s);
int m;
scanf("%d",&m);
int w=0;
memset(ok,1,sizeof(ok));
int i,k;
for (i=m;i<len;i++)
{
char minn=s[i];
int pos=i;
for (k=i-1;k>=0;k--)
{
if (ok[k]==0)
break;
if (s[k]<=minn)
{
minn=s[k];
pos=k;
}
}
ok[pos]=0;
ans[w++]=minn;
}
int flag=0;
for (i=0;i<w;i++)
{
if (flag==0 && ans[i]=='0')
continue;
printf("%c",ans[i]);
flag=1;
}
if(flag==0) printf("0");
printf("\n");
}
return 0;
}
注:將此源程序提交給HDU 3183 A Magic Lamp(http://acm.hdu.edu.cn/showproblem.php?pid=3183)可以Accepted。
【例3】最小系數和。
問題描述
設有如下的等式:a0+a1*21+a2*22+...+am*2m=n。給定整數n和m,問等式成立時,各系數和a+a1+…+am的最小值為多少?
輸入
有多個測試用例。輸入的第一行包含一個整數T(1≤T≤105),表示測試用例的數量。
對於每個測試用例:包含兩個整數n和m(0≤n、 m≤109).
輸出
對於每個測試用例,輸出各系數和a+a1+…+am的最小值。
輸入樣例
10
1 2
3 2
5 2
10 2
10 3
10 4
13 5
20 4
11 11
12 3
輸出樣例
1
2
2
3
2
2
3
2
3
2
(1)編程思路。
因為n的范圍為109,所以m最大到31就夠了,因為231已經超過了109。因此當m超過31時,系數a32~am均為0。
貪心策略是:由於1*24=2*23,即a4=1和a3=2等價,因此應盡可能用2的高次方來表示n。即從系數am開始考慮,n中盡可能減掉足夠的2m,之后減去足夠多的2m-1,直到n減為0。
(2)源程序。
#include <stdio.h>
int power(int x,int n)
{
int p=1,i;
for (i=1;i<=n;i++)
p*=x;
return p;
}
int main()
{
int t;
scanf("%d",&t);
while (t--){
int n,m,sum,i;
scanf("%d%d",&n,&m);
if (m>31) m=31;
sum=0;
for (i=m;i>=0;--i){
int cnt=power(2,i);
sum+=n/cnt;
if (n%cnt==0) break;
n%=cnt;
}
printf("%d\n",sum);
}
return 0;
}
注:將此源程序提交給HDU5747 Aaronson(http://acm.hdu.edu.cn/showproblem.php?pid=5747)可以Accepted。
【例4】不進位加法。
問題描述
給出兩個長度不超過 106 的數字串,將這兩個數字串各自打亂后隨機組合,但是打亂重組后不能有前導0。然后將這兩個數字串相加,相加的規則是每一位相加,但不進位。例如,計算4567+5789時,結果為9246,計算1234+9876,結果將為0。
問能夠得到最大的結果串是多少?
輸入
第1行有一個數字T(T<=25),表示測試用例的數量。
對於每個測試用例,有兩行:第一行是數字A,第二行是數字B。
A和B的位數相同,且無前導零。
輸出
對於測試用例X,首先輸出“case #X:”,然后輸出不帶前導零的最大可能和。
輸入樣例
1
5958
3036
輸出樣例
Case #1: 8984
(1)編程思路。
貪心策略是:從高位到低位,每次貪心選擇相加起來最大的數字。
在貪心的過程中,不去枚舉加數和被加數,只枚舉結果。先分別保存兩個數字串中,數字0~9各自出現的次數。由於加法不進位,因此最大可能和的位數應與數字串的位數相同。因此,最大可能和這一結果從高位到低位,依次看能否用兩個數字串的數字組成9,不能再看能否組成8,之后是7,…。
最高位要單獨處理,因為最高位要求被加數和加數都不能為0。
(2)源程序。
#include <stdio.h>
#include <string.h>
#define MAXN 1000005
char str1[MAXN],str2[MAXN];
int ans[MAXN];
int main()
{
int t;
int iCase = 0;
scanf("%d",&t);
while (t--)
{
iCase++;
scanf("%s%s",str1,str2);
int n = strlen(str1);
if (n == 1)
{
printf("Case #%d: %d\n",iCase,(str1[0]-'0'+str2[0]-'0')%10);
continue;
}
int a[10],b[10]; // a[i](或b[i])保存加數A(或B)中數字i的個數
memset(a,0,sizeof(a));
memset(b,0,sizeof(b));
int i,j,k;
for (i = 0;i < n;i++)
{
a[str1[i]-'0']++;
b[str2[i]-'0']++;
}
// 首位不能為前導0,因此特別處理
int x =0, y=0;
int ttt = -1;
for (i = 1;i <= 9;i++)
for (j = 1;j <= 9;j++)
if (a[i]!=0 && b[j]!=0 && ((i+j)%10) > ttt)
{
x = i;
y = j;
ttt = (x+y)%10;
}
a[x]--;
b[y]--;
int cnt = 0;
ans[cnt++] = (x+y)%10;
int digit;
for (digit=9;digit>=0;digit--)
{
for (i=0;i<=9;i++)
if(a[i]!=0)
{
if (i<=digit)
{
j = digit-i;
k = a[i]<b[j]?a[i]:b[j];
a[i] -= k;
b[j] -= k;
while (k--)
ans[cnt++] = digit;
}
j = 10 + digit - i;
if (j > 9) continue;
k = a[i]<b[j]?a[i]:b[j];
a[i] -= k;
b[j] -= k;
while (k--)
ans[cnt++] = digit;
}
}
printf("Case #%d: ",iCase);
for (i=0;i<cnt-1;i++)
if (ans[i]!=0) break;
for (;i<cnt;i++)
printf("%d",ans[i]);
printf("\n");
}
return 0;
}
注:將此源程序提交給HDU4726 Kia's Calculation(http://acm.hdu.edu.cn/showproblem.php?pid=4726)可以Accepted。
【例5】最大的位或。
問題描述
給定自然數l和r,選取2個整數x,y滿足l <= x <= y <= r,使得x|y最大。
其中|表示按位或,即C、 C++、 Java中的|運算。
輸入
包含至多10001組測試數據。
第一行有一個正整數,表示數據的組數。
接下來每一行表示一組數據,包含兩個整數l,r。
保證 0 <= l <= r <= 1018。
輸出
對於每組數據輸出一行,表示最大的位或。
輸入樣例
5
1 10
0 1
1023 1024
233 322
1000000000000000000 1000000000000000000
輸出樣例
15
1
2047
511
1000000000000000000
(1)編程思路。
對於或運算“ | ”,滿足a | b >= max(a,b)。
因此,對於給定的自然數l和r,r不需要改變,只需改變l的值。把l和r寫成二進制的形式進行分析,可知:
1)從高位起,找到兩個數第一個不相同的位p,對於前面的高位,如果為0,則不能改為1,因為會超出范圍;如果為1,則不應改為0,因為其值會變小。
2)對於找到的第一個不相同的p位,r對應的二進制的p位必為1,l的p位必為0。所以結果的p位取1。
3)由於在p位時,r為1,l為0,因此即使l在后面的位全部取值為1,都滿足l<r。因此,從p+1位開始,l任意的0都可以取值為1,使得結果p位后的每一位都為1。這樣,可以得到最大的或值。
4)因此,結果為l和r兩個數高位相同的部分 + 后一部分全部取值為1。
(2)源程序。
#include <stdio.h>
int main()
{
int t;
scanf("%d",&t);
while(t--) {
long long l, r, ans = 0;
scanf("%lld%lld",&l,&r);
long long x = r,t1,t2;
int d = -1;
while (x) { // 求出r對應的二進制數的位數
d++;
x >>= 1;
}
int i;
for (i = d; i>=0; i--) {
t1 = (l>>i)&1;
t2 = (r>>i)&1;
if (t1==t2) ans += t1*(1LL<<i);
else break;
}
if (i>=0) ans += (1LL<<(i+1))-1;
printf("%lld\n",ans);
}
return 0;
}
注:將此源程序提交給HDU 5969 最大的位或(http://acm.hdu.edu.cn/showproblem.php?pid=5969)可以Accepted。
【例6】電波。
問題描述
阿里正在做一個物理實驗,要求他觀察一個電波。他需要進一步研究每個峰值和谷值的高度(峰值表示該值嚴格大於其相鄰值,谷值表示該值嚴格小於其相鄰值)。他確實把這些數字寫了下來,但他太粗心了,以至於把它們寫在一行中,沒有分開,例如“712495”可能代表“7 12 4 9 5”。他能記得的唯一信息是:
1)數據以谷值開始;
2)每個值都是峰值或谷值。
現在他想插入空格以使數據有效。如果存在多個解決方案,他將選擇空白較多的解決方案。
輸入
輸入由幾個測試用例組成。
第一行包含一個整數N(1<=N<=100),即數據的長度。
第二行包含一個字符串S,即他記錄的數據。S只包含數字。
輸出
一個整數,所能插入的最多空格數。
輸入樣例
6
712495
輸出樣例
4
(1)編程思路。
為了盡可能多地插入空格,顯然波谷和波峰都用一位數最佳。按照貪心的思想,波谷一定是一位數,波峰一位數不夠大的時候加入到兩位數就一定夠大了的。
具體處理時,當在尋找波谷碰到零時,零就自然當成波谷;當在尋找波峰碰到零時,將前面的波谷加到前一個波峰上,讓當前的零做波谷,使得波谷的值盡量小。
源程序中,變量a表示前一個值,b表示當前考慮的值,tag為偶數時表示正在尋找波谷,奇數時在尋找波峰。
(2)源程序。
#include <stdio.h>
int main()
{
int n;
while (scanf("%d", &n) != EOF)
{
char data[101];
scanf("%s", data);
int a, b,tag = 0;
a = 11;
b = 0;
int i,ans = 0;
for (i = 0; i < n; i++)
{
b = (data[i] - '0');
if (tag % 2 == 0){ // 確定波谷
if (b < a){
a = b;
}
else {
i++;
a=data[i]-'0';
}
}
else { // 確定波峰
if (b > a) {
a = b;
}
else{
if (b == 0){
while(data[i] == '0'){
i++;
if (i >= n) break;
}
a = 0; // 0做波谷
b = data[i]-'0';
a = b;
}
else{
i++;
a = b*10 + (data[i] - '0');
}
}
}
if (i >= n) break;
ans ++; tag ++;
}
printf("%d\n", ans-1);
}
return 0;
}
注:將此源程序提交給HDU 4105 Electric wave(http://acm.hdu.edu.cn/showproblem.php?pid=4105)可以Accepted。
【例7】對子和順子。
問題描述
給出n個整數a1、a1、…、an,ai(1≤ai≤n)。
兩個相同的數字(例如:2和2)可構成一個對子,三個連續的整數(如:2,3,4)可構成一個順子。
現在請你用給定的這些整數來盡可能多的構成順子和對子。注意,每個整數只能使用一次。
輸入
輸入包含幾個測試用例。
對於每個測試用例,第一行包含一個整數n(1≤N≤106)。.
然后下一行包含n個空格分隔的整數ai(1≤ai≤n)。
輸出
對於每個測試用例,在一行中輸出可構成順子和對子的總數。
輸入樣例
7
1 2 3 4 5 6 7
9
1 1 1 2 2 2 3 3 3
6
2 2 3 3 3 3
6
1 2 3 3 4 5
輸出樣例
2
4
3
2
(1)編程思路。
定義數組int a[1000001],其中a[i]保存給定整數中i的個數。輸入時統計每個整數i出現的次數。這樣可以對數組a按下標從小到大遍歷,對應的整數也正好從小到大。
由於一個對子用2個整數,一個順子用3個整數。因此,優先考慮對子。
前兩個整數不可能存在順子,所以挑對子。從第3個整數開始,先判斷能否與前面的剩下的整數形成順子(只需要出一個整數就使得cnt+1),不能形成順子再考慮對子。
(2)源程序。
#include <stdio.h>
#include <string.h>
int a[1000001];
int main()
{
int n;
while (scanf("%d",&n)!=EOF){
memset(a,0,sizeof(a));
int i,x,max=0;
for (i=1;i<=n;i++){
scanf("%d",&x);
if (max<x) max=x;
a[x]++;
}
int ans=a[1]/2;
a[1]=a[1]%2;
for (i=2;i<=max;i++)
{
if (a[i-1]==1 && a[i-2]==1 && a[i]>0){
a[i]--; a[i-1]--; a[i-2]--;
ans++;
}
ans+=a[i]/2;
a[i]%=2;
}
printf("%d\n",ans);
}
return 0;
}
注:將此源程序提交給HDU6188 Duizi and Shunzi(http://acm.hdu.edu.cn/showproblem.php?pid=6188)可以Accepted。
【例8】平衡字符串。
問題描述
有n個由“(”和“)”組成的字符串s1、s2、…、sn。將這n個字符串重新排序后,把它們連接起來,得到一個新的字符串t。設f(t)為t的最長平衡子序列(不必連續)的長度。所有可能t的f(t)的最大值為多少?
如下類型的字符串稱為平衡字符串:
(1)如果是空字符串;
(2)如果A和B是平衡的,AB是平衡的;
(3)如果A是平衡的,(A)是平衡的。
輸入
有多個測試用例。輸入的第一行包含一個整數T,表示測試用例的數量。
對於每個測試用例:
第一行包含一個整數n(1≤N≤105)表示字符串的數目。
接下來的n行中的每一行都包含一個字符串si,由“(”和“)”組成。
輸出
對於每個測試用例,輸出一個表示答案的整數。
輸入樣例
2
1
)()(()(
2
)
)(
輸出樣例
4
2
(1)編程思路。
先把每個字符串里面能匹配的括號全部配對抵消並計數,剩下的一定是由p個右括號+q個左括號組成的字符串。形如下列4種情況:)))))、 (((((((( 、))(((((( 、))))))))((。要使平衡子序列(不必連續)的長度最長,則需要最大化利用所有的剩余括號,即把左括號盡可能多的放在左邊,右括號盡可能多的放在右邊。
對於消除匹配的括號后的4種剩余串,如果只包含左括號(形如“((((((((”)則把它放在左邊,如果只有右括號(形如“)))))”)則把它放在右邊。對於形如“))((((((”、“))))))))((”的剩余串采用的貪心策略如下:
1)若兩個字符串都是左括號多右括號少,就讓右括號多的放右邊;
2)若兩個字符串都是左括號少右括號多,就讓左括號多的放左邊;
3)若一個字符串左括號多右括號少,一個左括號少右括號多,讓左括號多的放左邊;
4)若一個字符串左括號少右括號多,一個左括號多右括號少,讓左括號多的放左邊。
(2)源程序。
#include <stdio.h>
#include <algorithm>
using namespace std;
struct node
{
int left, right;
};
struct node a[100001];
char str[100001];
int cmp(struct node a, struct node b)
{
if (a.left<=a.right && b.left>b.right) return 0;
if (a.left>a.right && b.left<=b.right) return 1;
if (a.left<=a.right && b.left<=b.right) return a.left>=b.left;
return a.right<=b.right;
}
int main(void)
{
int t;
scanf("%d", &t);
while (t--)
{
long long ans = 0;
int n,i,j;
scanf("%d", &n);
for (i=0; i<n; i++)
{
a[i].left=a[i].right=0;
scanf("%s", str);
for (j = 0; str[j]!='\0'; j++)
{
if (str[j] == '(')
a[i].left++;
else
{
if (a[i].left)
{
a[i].left--;
ans++;
}
else
{
a[i].right++;
}
}
}
}
sort(a,a+n,cmp);
int cnt=a[0].left;
for (i = 1; i < n; i++)
{
if (cnt>= a[i].right)
{
ans += a[i].right;
cnt -= a[i].right;
}
else
{
ans += cnt;
cnt = 0;
}
cnt+=a[i].left;
}
printf("%lld\n", ans * 2);
}
return 0;
}
注:將此源程序提交給HDU6299 Balanced Sequence(http://acm.hdu.edu.cn/showproblem.php?pid=6299)可以Accepted。
【例9】自然數序列。
問題描述
給定自然數序列ai,ai∈[0,n],ai≠aj( i≠j ),求自然數序列bi,使ai與bi對應位上的每一個數進行異或運算后相加和最大。
輸入
有多個測試用例。
對於每個測試用例,第一行包含一個整數n(1≤ N≤ 105),第二行包含a0、a1、a2、…、an。
輸出
對於每個測試用例,輸出兩行。第一行包含最大的異或和。第二行包含n+1個整數b0、b1、b2、…、bn。每兩個整數間有一個空格。不要在bn之后輸出任何空格。
輸入樣例
4
2 0 1 4 3
輸出樣例
20
1 0 2 3 4
(1)編程思路。
為了使異或運算之后相加和最大,采用的貪心策略是:使每一個對應位異或后能取得最大值。兩個整數轉化為二進制時,若每一位互補時,它們異或后能取得最大值。例如5(00000101)和10(00001010)互補,6(00000110)和9(00001001)也互補。
因此本題的實質要求出0~n中每個整數的異或互補數。一個最簡單的策略是對於整數k的二進制形式,把0換成1,1換成0,就可得到它的互補數,兩個數亦或后將得到全1的最大數。
在具體求0~n中每個整數p的互補數q時,定義數組b[],其中b[i]的值表示整數i的互補數,數組b所有元素初始值全置為-1。然后從大到小求,即先求n的互補數,再求n-1的互補數,…。若某整數b[p]==-1,則求出p的互補數q,同時置b[p]=q,b[q]=p,之后不用求q的互補數了,即若某整數b[p]>=0,則表示其互補數已求出,直接跳過。
為什么從大到小求呢?因為在求互補數時,對於任意一個數p,只有兩種情況,一是其異或互補數q比它小,二是其異或互補數q比它大。如果異或互補數q比它小,則q對應的二進制數第一位肯定是0,后面可能會跟若干個0。最壞是全1這種(例如整數15),對應的是0;如果異或互補數q比它大,則q對應的二進制位數一定比p這個數長。例如,整數15(1111)對應的異或互補數可能是16(10000)、48(110000)等等。因此,從大到小求異或互補數,就可以避免上面不好處理的情況。例如,若n=48,先求48(110000)的互補數,為15(1111),再求47(101111)的互補數,為16(10000),…。若n=16,先求16(10000)的互補數,為15(1111),再求15(1111)的互補數,已求出為16(10000),跳過…。若n=15,先求15(1111)的互補數,為0(0000),再求14(1110)的互補數,為1(0001),…。
(2)源程序。
#include <stdio.h>
#include <string.h>
int main()
{
int n;
while (scanf("%d", &n) != EOF){
int a[100001], b[100001];
int i,k;
for (i = 0; i<=n; i++)
scanf("%d", &a[i]);
long long sum = 0;
memset(b,-1,sizeof(b));
int v,val,p;
for (i=n; i>=0; i--){
if (b[i]>=0)
continue;
k = i;
p = 1;
val = 0;
while (k!=0){
v = k & 1;
val+=(v^1)*p;
p <<= 1;
k >>= 1;
}
b[i] = val;
b[val] = i;
}
for (i=0; i<=n; i++)
sum += a[i]^b[a[i]];
printf("%lld\n", sum);
for (i=0; i<=n; i++)
{
if (i!=0) printf(" ");
printf("%d",b[a[i]]);
}
printf("\n");
}
return 0;
}
注:將此源程序提交給HDU 5014 Number Sequence(http://acm.hdu.edu.cn/showproblem.php?pid=5014)可以Accepted。
【例10】拆分。
問題描述
給定一個數自然數N(N<=109),將它拆分成若干各不相同的自然數Ai,滿足ΣAi=N,且要求ΠAi(累乘)最大。
現在你能解決這個問題嗎?(因為最后的數字太大,你的答案將是模10^9+7)
輸入
第一行是一個整數T,表示測試用例的數量。
然后跟着T行。每行包含一個整數N。
輸出
你能得到的最大累乘積(模為10^9+7)。
輸入樣例
2
4
20
輸出樣例
4
720
(1)編程思路。
要使累乘的積最大,則拆分出來的自然數(1得除外)的個數應盡可能多。
例如,9可拆分為2+7、4+5、2+3+4,顯然2+3+4的拆分方法最優。
因此,拆分的貪心策略為:盡可能將N拆分為以2為首項的連續遞增序列,即把N拆成2+3+…+x<=N。
先求出x,之后看N還多了多少,設R=N-(2+3+…+x),就把后面的R項依次+1,變成2+3+...+y+(y+2)+(y+3)+...+(x+1)。
特殊情況是,若從2到x都加1之后還剩余1,這時候把最后一項再加1,變成3+4+...+x+(x+2)。
為求累乘積方便,采用乘法逆元進行處理。
(2)源程序。
#include <stdio.h>
#include <string.h>
const long long MOD=1000000007;
int main()
{
long long f[45000];
int sum[45000];
int inv[45000];
inv[1]=1;f[1]=1;sum[1]=0;
int i;
for (i=2; i<45000;i++){
inv[i]=(MOD-MOD/i)*inv[MOD%i]%MOD; // 乘法逆元
f[i]=(f[i-1]*i)%MOD; // 前綴乘
sum[i]=sum[i-1]+i; // 前綴和(從2開始)
}
int t;
scanf("%d",&t);
while (t--){
int n;
scanf("%d",&n);
if(n<5) {
printf("%d\n",n);
continue;
}
int left=2;
int right=45000;
while (left+1<right){
int mid=(left+right)/2;
if (sum[mid]>n) right=mid;
else left=mid;
}
int k=n-sum[left];
long long ans;
if (2+k>left){
ans=f[left]*inv[2]%MOD*(k+2)%MOD;
printf("%lld\n",ans);
}
else{
ans=f[left]*inv[left+1-k]%MOD*(left+1)%MOD;
printf("%lld\n",ans);
}
}
return 0;
}
注:將此源程序提交給HDU 5976 Detachment(http://acm.hdu.edu.cn/showproblem.php?pid=5976)可以Accepted。
在理解了貪心法及其應用方法的基礎上,可以刷一下如下的HDU題庫中的10道題目。這幾道題目均可以采用貪心法解決。
HDU 2570 迷瘴(http://acm.hdu.edu.cn/showproblem.php?pid=2570)

// 要讓最后的體積最大,需要先加入濃度小的。貪心策略是:按濃度從小到大加入。 #include <stdio.h> int main(void) { int c; scanf("%d",&c); while(c--) { int n,v,w; scanf("%d%d%d",&n,&v,&w); int i,j,p[101]; for(i=0; i<n; i++) scanf("%d",&p[i]); for (i=0;i<n-1;i++) for (j=0;j<n-1-i;j++) if (p[j]>p[j+1]) { int t=p[j]; p[j]=p[j+1]; p[j+1]=t; } double sum=0.0; for (i=0; i<n; i++) { if (sum+p[i] <= w*(i+1)) sum+=p[i]; else break; } if(!sum) printf("0 0.00\n"); else printf("%d %.2f\n",v*i,sum/(i*100)); } return 0; }
HDU3177 Crixalis's Equipment(http://acm.hdu.edu.cn/showproblem.php?pid=3177)

// 當前物品能否放進山洞取決於當前物品的的移動體積是否小於山洞當前的剩余體積。貪心策略是:每次向山洞中放移動體積和停放體積差值最大的那個物品。 #include <stdio.h> struct node { int a,b; }; int main() { int t; scanf("%d",&t); while (t--){ int v,n; scanf("%d%d",&v,&n); struct node e[1001]; int i,j; for (i=0;i<n;i++){ scanf("%d%d",&e[i].a,&e[i].b); } for (i=0;i<n-1;i++){ for (j=0;j<n-1-i;j++){ if (e[j].a+e[j+1].b>e[j+1].a+e[j].b){ struct node tmp; tmp=e[j]; e[j]=e[j+1]; e[j+1]=tmp; } } } for (i=0;i<n;i++) { if (v<e[i].b) break; v-=e[i].a; } if (i<n || v<0) printf("No\n"); else printf("Yes\n"); } return 0; }
HDU3348 coins(http://acm.hdu.edu.cn/showproblem.php?pid=3348)

// 求最少的數量采用的貪心策略是:優先取面值盡量大的紙幣來湊這個價格。 // 求最多的數量通過貪心來湊出sum-n(sum為所有給定紙幣的總價格,n為題目要求湊的價格)的最小紙幣數x,則湊出n的最大紙幣數 = 總紙幣數 – x。 #include <stdio.h> int v[5] = {1,5,10,50,100}; int a[5]; int calc(int value) { int res = 0; int i,t; for (i = 4;i >=0;i--) { if (value >= v[i]) { t = (value/v[i])<a[i]?value/v[i]:a[i]; value -= v[i] * t; res += t; } } if (value != 0) return -1; return res; } int main() { int t; scanf("%d",&t); while (t--) { int p,i; int sum = 0, num = 0; scanf("%d",&p); for (i=0;i<5;i++) { scanf("%d",&a[i]); num += a[i]; sum += v[i] * a[i]; } int res_min = 0, res_max = 0; res_min = calc(p); res_max = calc(sum - p); if (res_min == -1 || res_max == -1) printf("-1 -1\n"); else printf("%d %d\n",res_min,num - res_max); } return 0; }
HDU 3661 Assignments(http://acm.hdu.edu.cn/showproblem.php?pid=3661)

// 貪心策略是:將數組A從小到大排序,數組B從大到小排序。然后逐項匹配,每次讓A中的最小和B中的最大匹配。 #include <stdio.h> int main() { int n,t; while(scanf("%d%d",&n,&t)!=EOF) { int a[1001],b[1001]; int i,j,tmp; for (i=0;i<n;i++) scanf("%d",&a[i]); for (i=0;i<n;i++) scanf("%d",&b[i]); for (i=0;i<n-1;i++) for (j=0;j<n-1-i;j++) if (a[j]>a[j+1]) { tmp=a[j]; a[j]=a[j+1]; a[j+1]=tmp; } for (i=0;i<n-1;i++) for (j=0;j<n-1-i;j++) if (b[j]<b[j+1]) { tmp=b[j]; b[j]=b[j+1]; b[j+1]=tmp; } int ans=0; for (i=0;i<n;i++) { if (a[i]+b[i]>t) ans += a[i]+b[i]-t; } printf("%d\n",ans); } return 0; }
HDU 3979 Monster(http://acm.hdu.edu.cn/showproblem.php?pid=3979)

// 設打第i只怪獸的耗時為ti,怪獸攻擊力為gi, // 貪心策略是按gi/ti的比值由大到小的順序消滅怪獸。 #include <stdio.h> #include <algorithm> using namespace std; struct Node { int hp,g; }; int n,m; int cmp(struct Node a,struct Node b) { return a.hp*b.g < b.hp*a.g; } int main() { struct Node monster[10010]; int t,iCase; scanf("%d",&t); for (iCase=1;iCase<=t;iCase++){ int n,m,i; scanf("%d%d",&n,&m); for (i=0;i<n;i++) { scanf("%d%d",&monster[i].hp,&monster[i].g); monster[i].hp = (monster[i].hp + m - 1)/m; } sort(monster,monster+n,cmp); long long ans = 0; int cnt = 0; for(int i = 0;i < n;i++) { cnt += monster[i].hp; ans += (long long)cnt*monster[i].g; } printf("Case #%d: %lld\n",iCase,ans); } return 0; }
HDU 4310 Hero(http://acm.hdu.edu.cn/showproblem.php?pid=4310)

// 貪心策略是:將血量少攻擊高的敵人優先打死。也就是按DFS/HP的比值從大到小排序,將比值大的敵人優先殺死。 #include <stdio.h> struct node { long long dps,hp; }; int main() { int n; struct node a[21]; while (scanf("%d",&n)!=EOF){ int i,j; long long sum=0; for (i=0;i<n;i++){ scanf("%lld%lld",&a[i].dps,&a[i].hp); sum+=a[i].dps; } for (i=0;i<n-1;i++) for (j=0;j<n-1-i;j++) if (a[j].dps*a[j+1].hp<a[j+1].dps*a[j].hp){ struct node tmp; tmp=a[j]; a[j]=a[j+1]; a[j+1]=tmp; } long long ans=0; for (i=0;i<n;i++){ ans+=sum*a[i].hp; sum-=a[i].dps; } printf("%lld\n",ans); } return 0; }
HDU4803 Poor Warehouse Keeper(http://acm.hdu.edu.cn/showproblem.php?pid=4803)

// 由於y的范圍比較大,無法用搜索方法去解決。 // 對於兩種操作,x+1,斜率(單價)不會發生改變,而y+1,會使得斜率變大。 // 執行x+1的次數是固定的(x-1)次,所以只要執行第二個操作的最小次數就行。 // 采取的貪心策略是:每次都把y加到符合最大的斜率。 #include <stdio.h> #define eps 1e-6 int main(void) { int x; double y; while (scanf("%d%lf",&x,&y)!=EOF){ if (1.0*x>y) { // 由於斜率始終是增加的,不會小於1 printf("-1\n"); continue; } double dy = 1.0; double maxk = (y+1-eps)/x; // 代表目標最大的斜率。因為只顯示整數部分,所以,y.9999999是最大的。 int ans = x-1; int i; for (i=1;i<=x;i++){ int add = i*maxk-dy; // 表示當前坐標能達到的最大y所增加的。 ans += add; dy += add; dy += (dy/i); // 執行i+1后y的變化。 } printf("%d\n",ans); } return 0; }
HDU4864 Task(http://acm.hdu.edu.cn/showproblem.php?pid=4864)

// 由於在報酬構成中,x的權重遠大於y的權重。因此將任務按照x降序排列,x相同時按照y降序排列。對於一個任務可以選擇的是所有x1>x的機器,而在這些機器中選擇最小的y1,因為這樣選就可以保留比較大的y1使其去匹配后面的任務。 #include <stdio.h> #include <string.h> #include <algorithm> using namespace std; struct node { int x, y; }; struct node machine[100001], task[100001]; int cmp(struct node a,struct node b) { if(a.x!=b.x) return a.x > b.x; return a.y > b.y; } int main() { int n,m; while (scanf("%d%d", &n,&m)!=EOF) { int i,j,k; for (i=0; i<n; i++) scanf("%d%d", &machine[i].x, &machine[i].y); for (i=0; i<m; i++) scanf("%d%d", &task[i].x, &task[i].y); sort(machine, machine+n,cmp); sort(task, task+m,cmp); int num = 0; long long money = 0; int s[150]; memset(s, 0, sizeof(s)); j = 0; for (i=0; i<m; i++) { while(j<n && machine[j].x>=task[i].x) { s[machine[j].y]++; j++; } for (k=task[i].y; k<=100; k++) if(s[k]>0) { num++; money += 500*task[i].x + 2*task[i].y; s[k]--; break; } } printf("%d %lld\n", num, money); } return 0; }
HDU 6563 Strength(http://acm.hdu.edu.cn/showproblem.php?pid=6563)

#include <stdio.h> #include <algorithm> using namespace std; struct node{ long long s, v; }; struct node a[100001], b[100001]; int cmp(struct node x, struct node y) { return x.s<y.s; } int main() { int t,iCase; scanf("%d",&t); for (iCase = 1; iCase <= t; iCase++){ int n,m; scanf("%d%d",&n,&m); int i,num = 0; for (i=0;i<n;i++) scanf("%lld",&a[i].s); for (i=0;i<m;i++) scanf("%lld",&b[i].s); for (i=0;i<m;i++) scanf("%lld",&b[i].v); long long res1 = 0, res2 = 0; int left = 0, right = n-1; sort(a, a+n,cmp); sort(b, b+m,cmp); while (left<m && right >= 0){ while (left<m && b[left].v) left++; if (left>=m || b[left].s > a[right].s) break; res1 += (a[right].s - b[left].s); right--; left++; } left = m-1, right = n-1; while (left >=0 || right >= 0){ if (right<0) break; if (a[right].s < b[left].s){ res2 = 0; break; } else if(!b[left].v){ res2 += a[right].s - b[left].s; } else if(left<0) res2 += a[right].s; if (left>=0) left--; if (right>=0) right--; } printf("Case %d: %lld\n",iCase,res1>res2?res1:res2); } return 0; }
HDU 6709 Fishing Master(http://acm.hdu.edu.cn/showproblem.php?pid=6709)

// 設num=∑(a[i]/k), // 若num>=(n-1),此時每烹飪一條魚i,同時抓(a[i]/k)條魚即可保證除去K外的每一份時間都在烹飪魚。 // 若num<(n-1),此時,要使浪費的時間最少,則需以(k-a[i]%k)從小到大的順序抓捕和烹飪魚。 #include <stdio.h> #include<algorithm> using namespace std; int k; int cmp(int x,int y) { return (k-x%k)<(k-y%k); } int main() { int t; scanf("%d",&t); while(t--){ int n; scanf("%d%d",&n,&k); int a[100001]; int num=0; long long ans=k; int i; for (i=1;i<=n;i++){ scanf("%d",&a[i]); num+=a[i]/k; ans+=a[i]; } if (num<n-1){ sort(a+1,a+1+n,cmp); num=n-num-1; for (i=1;i<=num;i++){ ans+=(k-a[i]%k); } } printf("%lld\n",ans); } return 0; }