【斜率優化】Codechef July Challenge 2019——Hit the Coconuts

前言

能夠自己推出斜率優化的式子了...實屬難得...

不過定義和實現都是參考了別人的博客的,╮(╯▽╰)╭...


woc...寫博客的時候發現自己推的式子的變量有點問題.../難受

題目

All submissions for this problem are available.### Read problem statements in Hindi, Bengali, Mandarin Chinese, Russian, and Vietnamese as well.

Nikki has N

coconuts, she wants to prepare a special coconut soup for her best friend Mansi. In order to make this soup, she has to break Z

coconuts. For each coconut, there is a fixed number of times Nikki needs to hit it if she wants it to break. Nikki can only hit one coconut at the same time.

Their friend Sakshi is a troublemaker. This time, Sakshi shuffled the coconuts in some (unknown) way. You are given a sequence A1,A2,…,AN

with the following meaning: it is possible to label the coconuts 1 through N in such a way that for each valid i, the i-th coconut needs to be hit exactly Ai

times to break.

Nikki wants to prepare the soup as soon as possible, so she wants to minimise the number of times she has to hit coconuts in the worst case in order to break Z

coconuts. Formally, she wants to find a strategy of hitting coconuts, possibly depending on which coconuts broke after which hits, such that no matter which coconuts broke and when, it is guaranteed that after H hits, there will be Z broken coconuts, and there is no strategy with smaller H. Help Nikki find H

— the minimum required number of hits.

Input

  • The first line of the input contains a single integer Tdenoting the number of test cases. The description of T test cases follows.
  • The first line of each test case contains two space-separated integers N and Z .
  • The second line contains N space-separated integers A1,A2,…,AN .

Output

For each test case, print a single line containing one integer — the minimum required number of hits.

Constraints

  • 1≤T≤1,000
  • 1≤Z≤N≤10^3
  • 1≤Ai≤10^6 for each valid i
  • the sum of N⋅Z over all test cases does not exceed 3⋅10^6

Subtasks

Subtask #1 (10 points):

  • 1≤T≤100
  • 1≤N≤500
  • Z=1
  • 1≤Ai≤1,000 for each valid i
  • the sum of N⋅Z over all test cases does not exceed 3,000

Subtask #2 (90 points): original constraints

Example Input

2
2 1
50 55 
2 1
40 100

Example Output

55
80

Explanation

Example case 1: Nikki can choose one coconut and try to hit it 55 times. It will break either after the 50-th hit or after the 55-th hit.

Example case 2: Nikki can choose one coconut and hit it 40 times. If it does not break, the other coconut must be the one that takes 40 hits to break, so she should hit the other coconut 40 times. In total, she needs to hit coconuts at most 80 times.

中文題意

題目描述
Nikki有N個椰子。她想給她最好的朋友Mansi燉椰子湯。燉椰子湯需要打開Z個椰子。
每個椰子殼有不同的堅硬度,需要不同的敲擊次數才能打開。Nikki 每次只能敲一個椰子。
他們有一個朋友Sakshi 是個搗蛋鬼。這一次 Sakshi 悄悄地隨機打亂了椰子的順序。

已知打亂前椰子依次需要敲A1, A2,A3....An下才能打開。被Sakshi打亂後沒人知道每個椰子所對應的打亂前的位置,但可以確定的是椰子還是同樣的N個椰子,椰子被移動不會影響打開它所需的敲打次數。
Nkki想盡快燉好湯,所以她想知道在最壞的情況下她至少需要敲幾下才能打開Z個椰子。也就是說,她需要設計一個敲打椰子的策略,使得無論椰子被打亂成什麼順序,都能保證一共敲擊H以後必然有至少Z個椰子被打開。並且,不可能找到-一個策略能夠讓H的值更小。Nikki的策略可以依據某一次敲擊是否打開了某個椰子的情況來做決策。
請幫Nikki找到這個最小的H值。.
輸入格式
輸入數據第一行包含一個整數T,表示數據組數。接下來是T組數據。
每組數據第一行包含兩個整數 N和Z。
接下來的一行包含N個整數A1,A2,A3...An,意義見題目描述。
輸出格式
對於每組數據,輸出一行包含一個整數,表示最壞情況下至少需要敲多少下。

題目大意

有n個椰子,每個椰子有個“敲擊次數”Ai,對於椰子 i 你要敲Ai次才能敲開此椰子

求敲開z個椰子所需要的最小敲擊次數

分析

參考博客&特別鳴謝:Hit the Coconuts

假設現在有n個數,若想要保證能取出一個椰子,至少需要敲 n*min{A1,A2,A3...An}次

即至少需要敲 “(數的個數)*(這些數裏的最小值)”那麼多次
那麼把這些數從大到小排序(這樣當前敲的i號椰子就是前i個椰子中的最小值),則:

dp[ i ][ j ]:前 i 個裏面保證能取出 j 個椰子需要敲的次數
dp[ i ][ k ] = min( dp[ j ][ k − 1 ] + ( i − j ) × a[ i ] )

可用斜率優化解決。這裏的 a[ i ] 是單調遞減的。即斜率是單調遞減的。
那麼下凸殼維護的最優決策點是越來越靠左的,所以pop的是右端,即是一個單調棧。


我自己的【斜率優化式子的推導】,好像變量名有點問題,與代碼的變量名對不上...但是本蒟蒻不知道哪裏出了問題..._(:зゝ∠)_...

(一)推導斜率式子

j \leqslant l,其對應的dp值分別爲:

dp[ j ][ k-1 ]+( i - j ) * a[ i ],dp[ l ][ k-1 ]+( i - l ) * a[ i ]

若 j 的DP值比 l 的更優(更小),則:

dp[ j ][ k-1 ]+( i - j ) * a[ i ]\leqslant dp[ l ][ k-1 ]+( i - l ) * a[ i ]

移項化簡得:

\frac{dp[l,k-1]-dp[j,k-1]}{l-j}\geqslant a[ i ]

若滿足上式,則 l 可以踢出隊了(因爲j更優嘛)


(二)再考慮如何維護並取得DP值(也就是答案)

剛剛上面說了, a[ i ] 是單調遞減的,即斜率是單調遞減的
那麼下凸殼維護的最優決策點是越來越靠左的,所以pop的是右端

爲什麼呢?我可能太弱了,一開始沒看懂,後來豁然開朗2333...這裏我將整個過程想詳細講一講:

首先,要從去掉“不夠優秀”的點,及檢查tail與tail-1的斜率是否滿足>=a[ i ]

再解決新點的加入,從右往左來看,下凸殼的形狀仍是上凸的(由不凹到凹)

而從左往右來看,有的點會與i形成下凹(由凹到不凹),所以要從隊尾開始檢查,刪去不合法的點

例如下圖,tail不合法,出隊,然後又檢查tail-1,以此類推

而在下圖中,tail合法

所以(一)維護了下凸殼後,取對尾元素更新DP值,(二)再刪去加入i後會不合法的點,最後將i入隊,解決!

暴力DP代碼

//剛剛那個版本居然暴力都沒打對qwq... 
#include<cstdio>
#include<cstring>
#include<cmath>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int MAXN=1000,INF=0x3f3f3f3f;
ll a[MAXN+5],dp[MAXN+5][MAXN+5];
//dp[i][j]:前i個椰子敲j個所需的最小花費 
int n,z;
bool cmp(int a,int b)
{
	return a>b;
}
void Solve()
{
	sort(a+1,a+n+1,cmp);
	memset(dp,0x3f,sizeof(dp));
	for(int i=0;i<=n;i++)
		dp[i][1]=i*a[i];
	for(int i=1;i<=n;i++)
		for(int k=1;k<=i;k++)
			for(int j=1;j<i;j++)
				dp[i][k]=min(dp[i][k],dp[j][k-1]+(i-j)*a[i]);
	ll ans=INF;
	for(int i=z;i<=n;i++)
		ans=min(ans,dp[i][z]);
	printf("%lld\n",ans);
}
int main()
{
	int t;
	scanf("%d",&t);
	while(t--)
	{
		scanf("%d%d",&n,&z);
		for(int i=1;i<=n;i++)
			scanf("%lld",&a[i]);
		Solve();
	}
	return 0;
}

優化DP代碼

#include<cstdio>
#include<cstring>
#include<cmath>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int MAXN=1000,INF=0x3f3f3f3f;
const double eps=1e-10;
ll a[MAXN+5],que[MAXN+5],dp[MAXN+5][MAXN+5];
//dp[i][j]:前i個椰子敲j個所需的最小花費 
int n,z;
bool cmp(int a,int b)
{
	return a>b;
}
ll Y(int i,int cur)
{
	return dp[i][cur];
}
ll K(int i,int j,int cur)
{
	return (Y(i,cur)-Y(j,cur))/(i-j);
}
void Solve()
{
	sort(a+1,a+n+1,cmp);
	for(int i=0;i<=n;i++)
		dp[i][1]=i*a[i];
	for(int k=2;k<=z;k++)//式子中的"k-1"是相同的,則看成一個常數,循環做z次即可 
	{
		int head=0,tail=0;
		que[++head]=0;
		que[++tail]=k-1;
		for(int i=k;i<=n;i++)
		{
			while(head<tail&&K(que[tail],que[tail-1],k-1)>=a[i])
				tail--;
			int j=que[tail];
			dp[i][k]=dp[j][k-1]+(i-j)*a[i];
			while(head<tail&&K(que[tail],que[tail-1],k-1)>=K(que[tail],i,k-1))
				tail--;
			que[++tail]=i;
		}
	}
	ll ans=INF;
	for(int i=z;i<=n;i++)
		ans=min(ans,dp[i][z]);
	printf("%lld\n",ans);
}
int main()
{
	int t;
	scanf("%d",&t);
	while(t--)
	{
		scanf("%d%d",&n,&z);
		for(int i=1;i<=n;i++)
			scanf("%lld",&a[i]);
		Solve();
	}
	return 0;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章