题目表述
小码哥最近迷上虚拟主播,开始疯狂的d虚拟主播。
点赞+投币+收藏,众所周知,是三连,是支持主播们。小码哥在疯狂的看虚拟主播们的切片,在疯狂的给他们三连!
小码哥有个奇怪的习惯,他从不三连,而是将其切割成二连:点赞+投币或者一连:收藏这两个行为。
小码哥每天都想要进行 a 次二连点赞+投币和 b 次一连收藏这两个行为,每天小码哥要嘛不做任何事,要嘛一定会进行 a 次二连和 b 次一连。小码哥不想有任何作品被自己只完成了二连或者一连,也就是说在所有天数操作结束后,所有的作品要嘛没有被小码哥进行任何操作,要嘛进行了三连操作。但小码哥每天操作时并不关心之前是否对这个视频操作过。
现在告诉你每天小码哥想进行的多少次二连一连操作,为了保证最终小码哥操作完之后没有视频是被小码哥只做了二连或者一连操作,你能告诉小码哥,每天如何操作,对哪些视频分别进行二连一连操作,可以使得他最多可能在满足条件的情况下给多少个视频达成的三连吗?你只需要告诉小码哥最终最多可以给多少个视频三连,但不需要告诉小码哥每天的操作方案。
格式
输入格式:
第一行一个整数 n(1 <= n <= 106 )表示经过了 n 天。
接下来 n行每行两个整数 a,b 表示这一天想进行 a 次点赞+投币操作, b 次收藏操作。
设所有的 a 和 b 加起来为 m ,则保证 m <= 106 且 n x m <= 5 x 107
输出格式:
一行一个整数表示小码哥他最多可能给多少个视频达成的三连。
样例
输入:
4
1 1
2 1
0 1
2 0
输出:
3
样例解释:
第一天给视频1点赞投币,给视频2收藏。
第二天给视频1收藏,给视频2点赞投币,给视频3点赞投币。
第三天给视频3收藏。
第四天不进行操作。
此时总共3个视频完成了三连操作,且没有任何视频只被小码哥进行一连二连操作。
分析
这道题可简单描述为,一个a零件和一个b零件可以组装成一个c零件,每天提供若干个a和b,问n天之后可以最多合成多少个c。如果没有限制条件的话,那么就把所有的a、b加起来sum_a,sum_b,求min(sum_a,sum_b)。当然不会这么简单!它有两个重要的限制条件:
1、每天提供的a和b,要么都使用,要么都不用
2、最后没有剩余的a或b(所有的作品要嘛没有被小码哥进行任何操作,要嘛进行了三连操作。)
推论一:容易得出当天合成c之后如果有剩余的零件a或b,那么a == 0 || b==0。
动态规划
状态定义
dp[i][j]:前i天,剩余a的数量-剩余b的数量=j时,能合成的c的最大数量。由 推论一可知,
j>0: j为剩余a的数量
j<0: (-j)为剩余b的数量
其他定义:
a[i],b[i]:表示第i天提供的a和b数量。
diff[i] = a[i]-b[i]:第i天多余的a或b的数量(多余b的数量为-diff[i])
c[i] = min(a[i],b[i]):第i天能单独提供c的数量
状态转移方程
dp[i][j] = max(dp[i][j],dp[i-1][j]); //不使用第i天的a和b
dp[i][j] = max(dp[i-1][j],dp[i-1][j-diff[i]] + c[i] + c_count[i]);//使用第i天的a和b
最终答案为dp[n][0]
其中c_count[i]表示 从剩余j-diff[i]加上diff[i],能合成的c的数量(j和diff[i]都表示a和b的差值,所以只有当且仅当j和diff异号的时候,c_count[i] > 0,否则 == 0)计算方式如下:
c_count = (j > 0 && diff[i] > 0) || (j < 0 && diff[i] < 0)?0: min(abs(j),abs(diff[i]));
初始化
dp[0][0] = 0;
代码
#include<bits/stdc++.h>
using namespace std;
int main() {
int n;
scanf("%d",&n);
int a,b,diff[n+1],c[n+1],suma[n+2],sumb[n+2];
for(int i = 1 ; i <= n ; i++)
{
scanf("%d %d",&a,&b);
c[i] = min(a,b);
diff[i] = a-b;
}
suma[n+1] = 0,sumb[n+1] = 0;
for(int i = n; i >= 1 ; i--)
{
suma[i] = suma[i+1];
sumb[i] = sumb[i+1];
if(diff[i] > 0)
suma[i] += diff[i];
if(diff[i] < 0)
sumb[i] += (-diff[i]);
}
unordered_map<int,int> mp,mpp;
mp[0] = 0;
int ans = 0;
for(int i = 1 ; i <= n ; i++)
{
mpp = mp;
for(const auto& kv : mp)
{
int key = kv.first;
int value = kv.second;
int key1 = key + diff[i];
//剪枝
if(key1 < 0)
if(i+1 > n || suma[i+1] < (-key1)) continue;
if(key1 > 0)
if(i+1 > n || sumb[i+1] < key1) continue;
//求c_count
int c_count = 0;
if( !( (key > 0 && diff[i] > 0) || (key < 0 && diff[i] < 0) ) )
c_count = min(abs(key),abs(diff[i]));
//状态转移
if(!mp.count(key1))
mpp[key1] = c_count+value+c[i];
else
mpp[key1] = max(mp[key1],c_count+value+c[i]);
}
mp = mpp;
//ans = max(ans,mp[0]);
}
cout << mp[0];
return 0;
}
优化
空间:由于hash表dp[i]只用到了dp[i-1]的数据,所以我们用一维的滚动hash表,且j有正有负。
时间:每天提供的a和b我们能知道当天能提供a或b的净剩数量,比如(2,3),能提供1个b的净剩数量。(4,2)能提供2个a的净剩数量。用suma[i]表示i到n天能提供的a的净剩数量。这个净剩数量有什么用呢?可以用于剪枝,减少unordered_map的大小。
怎么剪枝?
我们知道j表示a和b的差,也就是a比b多多少或者b比a多多少。
不妨设j>0,dp[j]:a比b多j个的c的最大数量,如果在i之后n-i天里能提供最大b的净剩数量sumb[i] < j,那么当前状态就是无效状态,永远不可能使当前状态转j为0。所以舍去(如上述代码)