[CSP-J 2019]第三题纪念品 题解 by 对面龙哥

506 阅读6分钟

简要分析题目

刚看到题时我觉得很简单,每天都买涨幅最大的就好了,就是道很简单的贪心嘛,然后看了一下别人的题解,用的全是DP,一看就是逊啦!

然后我就写了个超勇的贪心,结果……

贪心思路

处理每天的价格

由于每行输入代表一天的物价,所以可以把问题分到每一天,每输入一天物价处理一次。 首先用了一个类表示每天的价格和第二天的上涨量(也可能是下跌):


class ITEM {//将涨价单独列出方便排序
public:
	int id, value;
	ITEM() :id(0), value(0) {}
	inline void update(int i, int v) { id = i; value = v; return; }
};
inline bool cmp(ITEM, ITEM);//声明cmp函数
class DAY {
	int* price;//物品种类数不确定,用动态数组避免空间浪费
	ITEM* rise;
public:
	friend class WEI;//后面将小伟抽象为一个类,设为友元类方便WEI中成员函数访问
	friend inline bool cmp(ITEM,ITEM);//将cmp函数设为友元函数
	DAY() {}
	~DAY() {//析构函数,销毁对象时将对象申请的内存释放
		delete[]price;释放price指向的内存
		delete[]rise;释放rise指向的内存
	}
	inline void set() {初始化(第一天时没有前一天的价格来衡量涨价)
		price = new int[n];
		for (int i = 0; i < n; i++)price[i] = read();
		rise = new ITEM[n];
	}
	void update() {//每天更新价格
		int x;
		for (int i = 0; i < n; i++)price[rise[i].id] += rise[i].value;//根据昨天物价和涨价计算今天物价
		for (int i = 0; i < n; i++) {
			x = read();
			rise[i].update(i, x - price[i]);//根据明天物价和今天物价计算涨价
		}
		sort(rise, rise + n, cmp);根据涨幅排序
	}
}day_data;

于是就可以利用 update函数每天更新。

处理每天 -阿玮- 小伟的买卖行为

我们写出上文提到的小伟类,将买和卖统一,每次购买后就换算为第二天金钱,省去处理物品数量.

class WEI {
	int money;//小伟金钱
public:
	WEI() :money(0) {}
	inline void give_money(int x = read()) { money = x; return; }//初始化小伟金钱
	void update(const DAY& d = day_data) {//更新小伟的买卖行为后的金钱
		int temp = 0;
		for (int i = 0; day_data.rise[i].value > 0 && i < n && money; i++) {
			temp += (money / d.price[d.rise[i].id]) * (d.rise[i].value +
				d.price[d.rise[i].id]);//每次买完物品立即换算成第二天的价格存入temp
			money %= d.price[d.rise[i].id];//以今天价格扣除金钱
		}
		money += temp; return;剩余金钱和交易所得金钱相加得到最终金钱
	}
	inline void print() { printf("%d", money); return; }//输出小伟金钱
}_wei;

模拟

根据题意直接模拟即可:

int main() {
	t = read() - 1;
	n = read();
	_wei.give_money();
	day_data.set();
	for (int i = 0; i < t; i++) {
		day_data.update();
		_wei.update();
	}
	_wei.print();
	return 0;
}

贪心思路完整代码

代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int t, n;
inline int read() {
	int x = 0;
	char ch = getchar();
	while (ch > '9' || ch < '0')ch = getchar();
	while (ch >= '0' && ch <= '9') {
		x = (x << 1) + (x << 3) + (ch - 48);
		ch = getchar();
	}
	return x;
}
class ITEM {
public:
	int id, value;
	ITEM() :id(0), value(0) {}
	inline void update(int i, int v) { id = i; value = v; return; }
};
inline bool cmp(ITEM, ITEM);
class DAY {
	int* price;
	ITEM* rise;
public:
	friend class WEI;
	friend inline bool cmp(ITEM,ITEM);
	DAY() {}
	~DAY() {
		delete[]price;
		delete[]rise;
	}
	inline void set() {
		price = new int[n];
		for (int i = 0; i < n; i++)price[i] = read();
		rise = new ITEM[n];
	}
	void update() {
		int x;
		for (int i = 0; i < n; i++)price[rise[i].id] += rise[i].value;
		for (int i = 0; i < n; i++) {
			x = read();
			rise[i].update(i, x - price[i]);
		}
		sort(rise, rise + n, cmp);
	}
}day_data;
inline bool cmp(ITEM a, ITEM b) {
	return a.value ^ b.value ? a.value > b.value:day_data.price[a.id] 
		< day_data.price[b.id];
}
class WEI {
	int money;
public:
	WEI() :money(0) {}
	inline void give_money(int x = read()) { money = x; return; }
	void update(const DAY& d = day_data) {
		int temp = 0;
		for (int i = 0; day_data.rise[i].value > 0 && i < n && money; i++) {
			temp += (money / d.price[d.rise[i].id]) * (d.rise[i].value +
				d.price[d.rise[i].id]);
			money %= d.price[d.rise[i].id];
		}
		money += temp; return;
	}
	inline void print() { printf("%d", money); return; }
}_wei;
int main() {
	t = read() - 1;
	n = read();
	_wei.give_money();
	day_data.set();
	for (int i = 0; i < t; i++) {
		day_data.update();
		_wei.update();
	}
	_wei.print();
	return 0;
}

错误

这个代码只得了35分(洛谷),简单分析一下原因吧:

  • 不应该用上涨的量排序,应该用上涨的比例排序。
  • 金钱和价格不一定整除(大部分时候都不会整除),可能造成浪费。

所以直接贪心其实是得不到全局最优解的。

背包问题

所以其实这道题应该按照背包问题来做,每一天都以完全背包问题的方法处理一次( --所以人家的DP才是正解是吧,所以你的贪心其实本来就是错的,只能骗分? --读书人的代码能叫骗分吗?这叫先拿部分分!)

核心更改

问题出在小伟的购买方式上,所以只要修改小伟的 update函数即可:

void update(const DAY& d = day_data) {
	int* temp = new int[money + 1];
	for (int i = 0; i < n; i++) {
		for (int j = d.price[i]; j <= n; j++) {
			temp[j] = max(temp[j], temp[j - d.price[i]] + d.rise[i]);//背包问题状态转移方程
		}
	}
}

然后由于已经不需要从涨幅高到涨幅低购买,所以对涨幅排序已经不必要了,同时 ITEM 类也不需要了:

class DAY {
	int* price,* rise;
public:
	friend class WEI;
	DAY():price(0),rise(0){}
	~DAY() {
		delete[]price;
		delete[]rise;
	}
	void set() {
		price = new int[n];
		for (int i = 0; i < n; i++)price[i] = read();
		rise = new int[n];
		memset(rise, 0, n * 4);
	}
	void update() {
		for (int i = 0; i < n; i++) {
			price[i] += rise[i];
			rise[i] = read() - price[i];
		}
		return;
	}
}day_data;

稍作优化

这个代码已经可以AC了,但本着精(shu)益(ju)求(hao)精(kan)的原则,我们还要进行一点(负)优化。

让小伟在购买时可以对每一种物品进行判断,将不会涨价的物品直接排除掉,只要加一个 if 语句即可:

void update(const DAY& d = day_data) {
	int* temp = new int[money + 1];
	memset(temp, 0, (money + 1) * 4);
	for (int i = 0; i < n; i++) {
		if (d.rise[i] > 0){//若涨价量为正
		    for (int j = d.price[i]; j <= money; j++) {
			    temp[j] = max(temp[j], temp[j - d.price[i]] + d.rise[i]);
	    	}
	    }
	}
	money += temp[money];
}

这样就省去了一部分循环,在洛谷上实测优化前(开启O2优化)s用时在804~855ms之间, 优化后(开启O2优化)是在584~586ms之间,可以说是优化了很多。

总结

这道题其实就是完全背包问题,只要不想偏 -(说的就是我)- 基本都会做,需要思考的地方就是如何发现它是一个完全背包问题 -(而不是贪心)- 。当然如果有的人不会DP就另当别论了 -(不会DP去比什么赛呢)-

完整代码

代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int t, n;
inline int max(int a, int b) {
	return a > b ? a : b;
}
inline int read() {
	int x = 0;
	char ch = getchar();
	while (ch > '9' || ch < '0')ch = getchar();
	while (ch >= '0' && ch <= '9') {
		x = (x << 1) + (x << 3) + (ch - 48);
		ch = getchar();
	}
	return x;
}
class DAY {
	int* price,* rise;
public:
	friend class WEI;
	DAY():price(0),rise(0){}
	~DAY() {
		delete[]price;
		delete[]rise;
	}
	void set() {
		price = new int[n];
		for (int i = 0; i < n; i++)price[i] = read();
		rise = new int[n];
		memset(rise, 0, n * 4);
	}
	void update() {
		for (int i = 0; i < n; i++) {
			price[i] += rise[i];
			rise[i] = read() - price[i];
		}
		return;
	}
}day_data;
class WEI {
	int money;
public:
	WEI() :money(0) {}
	inline void give_money(int x = read()) { money = x; return; }
	void update(const DAY& d = day_data) {
		int* temp = new int[money + 1];
		memset(temp, 0, (money + 1) * 4);
		for (int i = 0; i < n; i++) {
			if (d.rise[i] > 0)for (int j = d.price[i]; j <= money; j++) {
				temp[j] = max(temp[j], temp[j - d.price[i]] + d.rise[i]);
			}
		}
		money += temp[money];
	}
	inline void print() { printf("%d", money); return; }
}_wei;
int main() {
	t = read() - 1;
	n = read();
	_wei.give_money();
	day_data.set();
	for (int i = 0; i < t; i++) {
		day_data.update();
		_wei.update();
	}
	_wei.print();
	return 0;
}