题解:P17094 [ICPC 2017 Qingdao R] Collecting Cents
lailai0916 · · 题解
题意简述
初始有
解题思路
先把每年需要考虑的存款方案缩到常数规模。
同一时间、同一存期的两笔存款合并后不会更差,因为:
再考虑一笔存期
因此,多年期存款的本金只需考虑
若存期
设当前还剩
先处理单年动作。
设当前现金为
括号内只取决于当前余数和动作,代码将其预处理为 cst。若原来还有一年后到期的金额
定义多年期存款的到期金额为:
对固定的
若动作
代码不使用递归生成动作,而是把
再处理跨年份状态。
状态包含当前现金,以及未来 key。一年转移时,最低
每个到期位置至多合并前
生成下一层状态后,先按 key 做基数排序。相同 key 只保留现金最大的状态。
不同 key 之间也存在支配关系。设状态
若逐年更新时差额从未变成负数,则
这种支配还给出现金值域上界。两个保留状态的现金差小于较差状态全部待到期金额之和,即小于
设动作剪枝后每类至多保留 key 去重后有
正确性证明
合并同存期存款、从多年期存款中拆出
动作剪枝中,支配动作使用的本金更少,并且截至每个未来时刻累计到账金额更多。它可以先保留多余资金,再完全复制被删动作之后的决策,所以删除被支配动作不会删去所有最优策略。
状态去重只在待到期序列完全相同时保留更多现金的状态,因此不会损失更优解。跨状态支配使用的差额递推是实际差额的下界;若下界始终非负,支配状态便能逐年补足另一状态的全部到期收入并复制其决策。因此状态剪枝同样安全。
从初始状态开始按年份归纳,每层转移都枚举了某个最优策略的下一步,随后两种剪枝仍保留一个不劣状态。第
参考代码
#include <bits/stdc++.h>
using namespace std;
using ull=unsigned long long;
const int D=7;
const int B=6;
const ull base=1ULL<<B;
struct choice
{
ull key;
int sum,cst;
};
struct state
{
ull key,csh;
};
struct full
{
ull key;
int sum,cst;
int pre[D];
};
int d,y;
bool vis[D];
vector<choice> ch[D][D][D];
vector<state> f,nxt,st,buf,cv,tmp;
int floor_div(int x)
{
return x>=0?x/d:-(-x+d-1)/d;
}
bool cover(const full &a,const full &b)
{
if(a.sum>b.sum)return 0;
for(int i=0;i<d;i++)if(a.pre[i]<b.pre[i])return 0;
return 1;
}
void init()
{
if(vis[d])return;
vis[d]=1;
for(int i=1;i<=d;i++)
{
int m=i;
int lim=1;
for(int j=2;j<=m;j++)lim*=d;
for(int j=0;j<d;j++)
{
int r=j;
vector<full> all,kp;
for(int k=0;k<lim;k++)
{
int cur=k,sum=0;
int z[D]={};
int p=2;
while(p<=m)
{
z[p]=cur%d;
cur/=d;
sum+=z[p];
p++;
}
int val=r-sum+floor_div(r-sum);
full x={0,sum,r-val,{}};
x.pre[0]=val;
p=2;
while(p<=d)
{
int q=p<=m?z[p]+z[p]*p/d:0;
x.key|=ull(q)<<(B*(p-2));
val+=q;
x.pre[p-1]=val;
p++;
}
all.push_back(x);
}
for(auto &x:all)
{
bool bad=0;
for(auto &q:kp)if(cover(q,x)){bad=1;break;}
if(bad)continue;
kp.erase(remove_if(kp.begin(),kp.end(),[&](const full &q){return cover(x,q);}),kp.end());
kp.push_back(x);
}
for(auto &x:kp)ch[d][m][r].push_back({x.key,x.sum,x.cst});
}
}
}
void radix_sort()
{
int k=(B*(d-1)+9)/10,siz=cv.size();
tmp.resize(siz);
int cnt[1024];
for(int i=0;i<k;i++)
{
memset(cnt,0,sizeof cnt);
int sh=i*10;
for(auto &x:cv)cnt[(x.key>>sh)&1023]++;
for(int j=1;j<1024;j++)cnt[j]+=cnt[j-1];
for(int j=siz-1;j>=0;j--)tmp[--cnt[(cv[j].key>>sh)&1023]]=cv[j];
cv.swap(tmp);
}
}
bool cover(const state &a,const state &b)
{
ull dif=a.csh-b.csh,x=a.key,y=b.key;
for(int i=0;i<d-1;i++)
{
dif+=dif/d;
int p=x%base,q=y%base;
if(p>=q)dif+=p-q;
else if(dif>=ull(q-p))dif-=q-p;
else return 0;
x>>=B;
y>>=B;
}
return 1;
}
void prune()
{
radix_sort();
st.clear();
for(int i=0;i<cv.size();)
{
int j=i+1;
ull csh=cv[i].csh;
while(j<cv.size()&&cv[j].key==cv[i].key)
{
csh=max(csh,cv[j].csh);
j++;
}
st.push_back({cv[i].key,csh});
i=j;
}
ull mx=0,mn=ULLONG_MAX;
for(auto &x:st)
{
mx=max(mx,x.csh);
mn=min(mn,x.csh);
}
vector<int> cnt(mx-mn+1);
for(auto &x:st)cnt[mx-x.csh]++;
int sum=0;
for(auto &x:cnt)
{
int val=x;
x=sum;
sum+=val;
}
buf.resize(st.size());
for(auto &x:st)buf[cnt[mx-x.csh]++]=x;
st.swap(buf);
nxt.clear();
for(auto &x:st)
{
bool bad=0;
for(auto &q:nxt)if(cover(q,x)){bad=1;break;}
if(!bad)nxt.push_back(x);
}
}
ull solve(ull n)
{
f.clear();
f.push_back({0,n});
for(int i=0;i<y;i++)
{
cv.clear();
int m=min(d,y-i);
for(auto &cur:f)
{
ull q=cur.csh/d,bck=cur.key%base;
int r=cur.csh%d;
for(auto &x:ch[d][m][r])
{
if(cur.csh<ull(x.sum))continue;
cv.push_back({(cur.key>>B)+x.key,cur.csh+q-x.cst+bck});
}
}
prune();
f.swap(nxt);
}
return f[0].csh;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin>>T;
while(T--)
{
ull n;
cin>>n>>d>>y;
init();
cout<<solve(n)<<'\n';
}
return 0;
}