★【动态规划】【决策单调性优化】【NOI2009】诗人小G

【问题描述】
	小G是一个出色的诗人,经常作诗自娱自乐。但是,他一直被一件事情所困扰,那就是诗的排版问题。
一首诗包含了若干个句子,对于一些连续的短句,可以将它们用空格隔开并放在一行中, 注意一行中可以放的句子数目是没有限制的。小G给每首诗定义了一个行标准长度(行的长度为一行中符号的总个数),他希望排版后每行的长度都和行标准长度相差不远。显然排版时,不应改变原有的句子顺序,并且小G不允许把一个句子分在两行或者更多的行内。在满足上面两个条件的情况下,小G对于排版中的每行定义了一个不协调度, 为这行的实际长度与行标准长度差值绝对值的P次方,而一个排版的不协调度为所有行不协调度的总和。
小G最近又作了几首诗,现在请你对这首诗进行排版,使得排版后的诗尽量协调(即不协调度尽量小),并把排版的结果告诉他。
【输入文件】
	输入文件poet.in包含多组数据。
第一行包含一个整数T,表示诗的数量,接下来是T首诗,这里一首诗即为一组数据。每组数据的第一行包含三个由空格分隔的正整数N、L、P,其中N表示这首诗句子的数目,L表示这首诗的行标准长度,P的含义见问题描述。从第2行开始,每行为一个句子,句子由英文字母、数字、标点符号等符号组成(ASCII码33~127, 但不包含 ‘-’)。
【输出文件】
	输出文件为poet.out。
对于每组数据,若最小的不协调度不超过1018,则第一行一个数表示不协调度,接下来若干行,表示你排版之后的诗。注意:在同一行的相邻两个句子之间需要用一个空格分开。如果有多个可行解,它们的不协调度都是最小值,则输出任意一个解均可。若最小的不协调度超过1018,则输出"Too hard to arrange"(不包含引号)。每组数据结束后输出"--------------------"(不包括引号),共20个"-","-"的ASCII码为45,请勿输出多余的空行或者空格。
【输入样例】
4
4 9 3
brysj,
hhrhl.
yqqlm,
gsycl.
4 9 2
brysj,
hhrhl.
yqqlm,
gsycl.
1 1005 6
poet
1 1004 6
poet
【输出样例】
108
brysj,
hhrhl.
yqqlm,
gsycl.
--------------------
32
brysj, hhrhl.
yqqlm, gsycl.
--------------------
Too hard to arrange
--------------------
1000000000000000000
poet
--------------------
【样例说明】
前两组输入数据中每行的实际长度均为6,后两组输入数据每行的实际长度均为4。一个排版方案中每行相邻两个句子之间的空格也算在这行的长度中(可参见样例中第二组数据)。每行末尾没有空格。
【评分方法】
本题设有部分分,当你的程序对于该测试点内每组数据计算得出的不协调度最小值都正确时,能得到本测试点70%的分数。在此情况下,若每组数据的排版方案都合法并且得出的不协调度都与输出的相等,则能得到本测试点剩下30%的分数。注意,输出格式错误可能会导致本测试点不得分。


【数据规模和约定】
总共10个测试点,数据范围满足:

测试点

T

N

L

P

1

10

18

100

5

2

10

2000

60000

10

3

10

2000

60000

10

4

5

100000

200

10

5

5

100000

200

10

6

5

100000

3000000

2

7

5

100000

3000000

2

8

5

100000

3000000

10

9

5

100000

3000000

10

10

5

100000

3000000

10

所有测试点中均满足句子长度不超过30。
50分算法:

1,2,3组数据:朴素动态规划,转移方程:
f[i] = min(f[j] + |s[i] - s[j] + i - j - 1 - L| ^ p)。
4,5组数据:贪心,当其中的s[i] - s[j] > 2L时,跳出循环(因为这样的话一定可以将它拆成两行并且更优)。

#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <string>
#include <cmath>

typedef long long int64;
const int maxN = 100010, maxL = 60010;
const int64 INF = 0x3f3f3f3f3f3f3f3fLL;
const int64 LIM = int64(1e18);
char poem[maxN][40]; int g[maxN], T, n;
//注意字符数组的大小要比实际字符串长度大,
//以便能够存储下末尾的'\0'。
int64 sum[maxN], len[maxN], f[maxN], p, max_len, L;

void printI64(int64 x)
{
	static const int64 MOD = 1000000000;
	if (x / MOD) printf("%d%09d", int(x / MOD), int(x % MOD));
	else printf("%d", int(x));
	return;
}

inline int64 pow(int64 a, int64 n)
{
	int64 ans = 1, tmp = a;
	for (; n; n >>= 1, tmp *= tmp) if (n & 1)
	{
		if ((double)ans * tmp > LIM) return INF;
		//如果超出10^18,则直接返回一个略大于10^18的值。
		ans *= tmp;
	}
	return ans;
}

void print(int i)
{
	if (g[i]) print(g[i]);
	for (int k = g[i] + 1; k < i; ++k)
		printf("%s ", poem[k]);
	printf("%s\n", poem[i]);
}

int main()
{
	freopen("poet.in", "r", stdin);
	freopen("poet.out", "w", stdout);
	scanf("%d", &T);
	while (T--)
	{
		scanf("%d%d%d", &n, &L, &p);
		max_len = ~INF;
		for (int i = 1; i < n + 1; ++i)
			scanf("%s", poem[i]), sum[i] = sum[i - 1] + strlen(poem[i]);
		f[0] = 0;
		for (int i = 1; i < n + 1; ++i)
		{
			f[i] = INF;
			for (int j = i - 1; j > -1; --j)
			{
				if (sum[i] - sum[j] > L << 1) break;
				int64 tmp = pow(std::abs(i - j - 1 + sum[i] - sum[j] - L), p);
				if (double(tmp) + f[j] < LIM && tmp + f[j] < f[i])
					f[i] = f[j] + tmp, g[i] = j;
			}
		}
		if (f[n] > LIM) printf("Too hard to arrange\n");
		else {printI64(f[n]); printf("\n"); print(n);}
		printf("--------------------\n");
	}
	return 0;
}

70分算法:

6,7组数据,p = 2,可以使用斜率优化将复杂度优化到线性。
设j为f[i]的最优决策,k是异于j的一个决策(k > j),t[i] = s[i] + i,则有
f[j] + (t[i] - t[j] - 1 - L) ^ 2 <= f[k] + (t[i] - t[k] - 1 - L) ^ 2,即:
f[j] + (t[i] - 1 - L) ^ 2 + t[j] ^ 2 - 2 t[j] · (t[i] - 1 - L)
<= f[k] + (t[i] - 1 - L) ^ 2 + t[k] ^ 2 - 2 t[k] · (t[i] - 1 - L),也即:
  f[j] - f[k] + t[j] ^ 2 - s[k] ^ 2
———————————————— >= 2 (t[i] - 1 - L)
             t[j] - t[k]
只需要用这个不等式来取舍结果就是了。

#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <string>
#include <cmath>
#define sqr(x) ((x) * (x))

typedef long long int64;
const int maxN = 100010, maxL = 60010;
const int64 INF = 0x3f3f3f3f3f3f3f3fLL;
const int64 LIM = int64(1e18);
char poem[maxN][40]; int g[maxN], T, n;
int64 t[maxN], sum[maxN], len[maxN], F[maxN], p, max_len, L;

void printI64(int64 x)
{
	static const int64 MOD = 1000000000;
	if (x / MOD) printf("%d%09d", int(x / MOD), int(x % MOD));
	else printf("%d", int(x));
	return;
}

inline int64 pow(int64 a, int64 n)
{
	int64 ans = 1, tmp = a;
	for (; n; n >>= 1, tmp *= tmp) if (n & 1)
	{
		if ((double)ans * tmp > LIM) return INF;
		ans *= tmp;
	}
	return ans;
}

void print(int i)
{
	if (g[i]) print(g[i]);
	for (int k = g[i] + 1; k < i; ++k)
		printf("%s ", poem[k]);
	printf("%s\n", poem[i]);
}

inline int work()
{
	F[0] = 0;
	for (int i = 1; i < n + 1; ++i)
	{
		F[i] = INF;
		for (int j = i - 1; j > -1; --j)
		{
			if (sum[i] - sum[j] > L << 1) break;
			int64 tmp = pow(std::abs(i - j - 1 + sum[i] - sum[j] - L), p);
			if (double(tmp) + F[j] < LIM && tmp + F[j] < F[i])
				F[i] = F[j] + tmp, g[i] = j;
		}
	}
	return 0;
}

#define check(j, k) \
(F[j] - F[k] + sqr(t[j]) - sqr(t[k]) \
 <= (t[j] - t[k]) * (t[i] - 1 - L) << 1)

#define cmp(j, k) \
((F[j] - F[k] + sqr(t[j]) - sqr(t[k])) * (t[k] - t[i]) <= \
 (F[k] - F[i] + sqr(t[k]) - sqr(t[i])) * (t[j] - t[k]))

inline int work2()
{
	static int q[maxN];
	int f = 0, r = 0;
	for (int i = 1; i < n + 1; ++i)
	{
		while (f < r && !check(q[f], q[f + 1])) ++f;
		F[i] = F[q[f]] + sqr(t[i] - t[q[f]] - L - 1);
		g[i] = q[f];
		while (f < r && !cmp(q[r - 1], q[r])) --r;
		q[++r] = i;
	}
	return 0;
}

int main()
{
	freopen("poet.in", "r", stdin);
	freopen("poet.out", "w", stdout);
	scanf("%d", &T);
	while (T--)
	{
		scanf("%d%d%d", &n, &L, &p);
		max_len = ~INF;
		for (int i = 1; i < n + 1; ++i)
			scanf("%s", poem[i]),
			sum[i] = sum[i - 1] + strlen(poem[i]),
			t[i] = i + sum[i];
		p == 2 ? work2() : work();
		if (F[n] > LIM) printf("Too hard to arrange\n");
		else {printI64(F[n]); printf("\n"); print(n);}
		printf("--------------------\n");
	}
	return 0;
}
满分算法:利用决策单调性优化。

不难得到,50分算法中的方程具有决策单调性(证明详见http://www.byvoid.com/blog/noi-2009-poet/)。
最开始所有点的最优决策决策都为0,即:
00000000000000000000000000000。
当f[1]被计算出来过后,有一部分点的最优决策变成了1,即:
00011111111111111111111111111。
同样,当f[2], f[3]被计算出来后,又有一部分点的最优决策改变,即:
00011111111111111122222333333,或:
00011111111111333333333333333。
但绝不会出现下列情况:
00011111113333333333333222222。
所以可以用一个栈来维护每一个最优决策区间,每计算出来一个f值,就可以用二分查找的方法更新,最终可以在O(n·log n)的复杂度内算出所有f值。
由于有二分查找的需要,所以不能允许当f值超过10^18时统统赋一个极大值(这样根本无法进行二分查找),解决方法是先用double类型算一遍,找出所有决策位置,再用long long类型进行最终结果的计算,同时用double类型计算的时候可以顺便判断最终结果是否超界。

#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <string>
#include <cmath>

typedef long long int64;
const int maxN = 100010, maxL = 60010;
const int64 INF = 0x3f3f3f3f3f3f3f3fLL;
const int64 LIM = int64(1e18);
struct Stack
{
	int L, R, pos; Stack() {}
	Stack(int L, int R, int pos): L(L), R(R), pos(pos) {}
} sta[maxN]; double F[maxN];
char poem[maxN][40]; int g[maxN], T, n, top, p;
int64 sum[maxN], f[maxN], len;

void printI64(int64 x)
{
	static const int64 MOD = 1000000000;
	if (x / MOD) printf("%d%09d", int(x / MOD), int(x % MOD));
	else printf("%d", int(x));
	return;
}

inline int64 pow(int64 a, int n)
{
	int64 ans = 1, tmp = a;
	for (; n; n >>= 1, tmp *= tmp) if (n & 1)
	{
		if (double(ans) * double(tmp) > LIM) return INF;
		ans *= tmp;
	}
	return ans;
}

inline double pow(double a, int n)
{
	double ans = 1, tmp = a;
	for (; n; n >>= 1, tmp *= tmp) if (n & 1) ans *= tmp;
	return ans;
}

void print(int i)
{
	if (g[i]) print(g[i]);
	for (int k = g[i] + 1; k < i; ++k) printf("%s ", poem[k]);
	printf("%s\n", poem[i]);
}

inline int64 getf(int i, int j)
{
	int64 tmp = pow(std::abs(i - j - 1 + sum[i] - sum[j] - len), p);
	return (double)tmp + f[j] > LIM ? INF : tmp + f[j];
}

inline double getF(int i, int j)
{return pow((double)std::abs(i - j - 1 + sum[i] - sum[j] - len), p) + F[j];}

inline void update(int i)
{
	while (i < sta[top].L && getF(sta[top].L, i)
			< getF(sta[top].L, sta[top].pos))
		sta[top - 1].R = sta[top].R, --top;
	//若栈定元素的左界的目前最优决策值比i更差,
	//那么直接出栈,同时右边界标记向下传。
	int L = sta[top].L, R = sta[top].R, p = R;
	if (i >= L) L = i + 1;
	while (L < R)
	{
		int Mid = (L + R) >> 1;
		if (getF(Mid, i) < getF(Mid, sta[top].pos)) p = R = Mid;
		else L = Mid + 1;
	}
	//在当前的栈顶所指定的区间(左闭右开区间)中找到决策转折点
	//即满足位置p的最优决策点为i,而p - 1的最优决策点不为i。
	if (p < sta[top].R)
		sta[top + 1] = Stack(p, sta[top].R, i), sta[top++].R = p;
	//若能找到这个转折点,则入栈。
	return;
}

int main()
{
	freopen("poet.in", "r", stdin);
	freopen("poet.out", "w", stdout);
	scanf("%d", &T);
	while (T--)
	{
		scanf("%d%d%d", &n, &len, &p);
		for (int i = 1; i < n + 1; ++i)
			scanf("%s", poem[i]), sum[i] = sum[i - 1] + strlen(poem[i]);
		sta[top = 1] = Stack(1, n + 1, 0); //栈的初始化。
		for (int i = 1, j = 1; i < n + 1; ++i)
		{
			if (i >= sta[j].R) ++j;
			F[i] = getF(i, g[i] = sta[j].pos);
			update(i);
		}
		if (F[n] > LIM) printf("Too hard to arrange\n");
		else 
		{
			for (int i = 1; i < n + 1; ++i) f[i] = getf(i, g[i]);
			//以long long类型重新计算f值。
			printI64(f[n]); printf("\n"); print(n);
		}
		printf("--------------------\n");
	}
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值