8.12 模拟赛记录
复盘
今天这比赛真的给我打破防了。
惯例开局遍历,看完了4道题,认为只有两个选择:
1 把T1这道我从没练过的数位DP强行做了
2 洗洗睡
T1数位DP学过但没练过,T2是期望+DP,需要算期望,当时考场上看到求概率有点蒙,就向后看了,T3好像可以DP但是想了一顿只能想到暴搜(而且我当时不知道为啥没看到n<=10有30分),T4完全想不到做法。综上所述,基本上也就T1勉强能做。
在反复思考了T3而仍然得不到DP方法之后,我直接花了两个半小时多写T1,但是显然我的悟性不够,有一个样例过不去,而且莫名其妙的打freopen之后RE了。在最后的20分钟突然意识到T3暴搜有30分,迅速写完暴搜(三个dfs)并迅速debug,能过样例,于是赶紧交了上去。
预计分数:0+0+30+0.
复盘分析
实际分数:0+0+25+0.
别的不提,这是我估的最接近的一次2333
T3之所以挂了5分,是因为搜索是O(n^s),对于n=10会被卡。T1我的思路非常接近正解了,但是不应该开01维存是否有前导0而是应该判后导0,或者判断一下后缀是否有能整除k的非0部分。这题据刷了很多数位DP的dalao说非常不同于一般的题,所以我觉得作为职业生涯第一道数位DP做到这程度就还行(捂脸),集训完了赶紧去练。以及此题打表或者暴力都能得30分,只能说我的思路还是受限了。
T2确实是我想复杂了,由于每一场比赛的结果互不影响,所以非常容易算概率,除以(m-1)就行了。另外需要打一个前缀和优化加和的过程,我觉得直接上这个优化不好理解,但如果从未优化的dp推过来还好理解一点。
T3我在最后一刻想到了解题的关键,使得取点可以随便取了。另外排个序做分段DP就行了。然而有意思的是这题求最短距离显然就Dijkstra跑正反图就可以了,但我不知道咋想的,愣是用dfs实现了这个功能(捂脸)。
T4就很复杂了,至今没整明白。
(部分)题解
T1设dp[i][j][0/1]表示填到右数第i位,填之前的数模m的结果为j,1/0表示后缀是否有整除m的非0后缀。
先算一下加上目前这位之后模m的结果作为要更新的第二维。如果发现模m为0且这位不是0,说明产生了合法的后缀,不然产生的是非法的后缀。不管这位到底如何,目前这位有合法后缀的情况都可以加上上一位有合法后缀的情况。
数位dp部分如下:
mi[0] = 1;
for(i = 1;i <= n;i++) mi[i] = (mi[i - 1] * 10) % m;//预处理
dp[0][0][0] = 1;
for(i = 1;i <= n;i++){
for(j = 0;j < m;j++){
for(k = 0;k <= 9;k++){
if(i == n && !k) continue;//最高位为0不合法
t = ((j + mi[i - 1] * k % m) % m);
if(!t && k) dp[i][t][1] = (dp[i][t][1] + dp[i - 1][j][0]) % mod;
else dp[i][t][0] = (dp[i][t][0] + dp[i - 1][j][0]) % mod;
dp[i][t][1] = (dp[i][t][1] + dp[i - 1][j][1]) % mod;
}
}
}
T2因为所有比赛的得分互不影响,所以每个人获得某得分的概率均相同,因此只需要算一个人的得某个分的概率。所以设dp[i][j]表示考虑到第i场比赛得j分的概率,对于第i场以前的,都用前缀和维护,这样就不必再去多一维求和。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
double dp[101][100001],sum[100001];
int main(){
int i,j,l,r,n,m,s = 0,a[101];
double t,E = 0;
scanf("%d %d",&n,&m);
for(i = 1;i <= n;i++){
scanf("%d",&a[i]);
s += a[i];
}
if(m == 1){
printf("1.0000000000000000\n");//特判,防止取概率RE
return 0;
}
t = 1.0 / (m - 1);//这里一定要用1.0而不是1
dp[0][0] = sum[0] = 1.0;
for(i = 1;i <= n;i++){
for(j = i;j <= i * m;j++){//到第i场为止的得分范围
l = max(i - 1,j - m),r = min(m * (i - 1),j - 1);
//这里算的是前面(i-1)场比赛的得分范围
dp[i][j] = t * sum[r];
if(l) dp[i][j] -= t * sum[l - 1];//取[l,r]的部分
if(l <= j - a[i] && j - a[i] <= r) dp[i][j] -= t * dp[i - 1][j - a[i]];//不能得a[i]分,需要去掉
}
for(j = 1;j <= i * m;j++) sum[j] = sum[j - 1] + dp[i][j];//更新前缀和
}
for(i = n;i < s;i++) E += dp[n][i];//统计总分不多于这个人的期望
printf("%.16lf\n",E * (m - 1) + 1);//最后加上本身
return 0;
}
T3跑两个Dijkstra,处理出某点到(b+1)和(b+1)到该点的最短路。显然,如果两个点处理同一任务,相当于这个点传到中转站一次,又从中转站传一次,同理,n个点处理同一任务,每个点都要发出(n-1)份文件,并收到(n-1)份,所以把两个最短路加在一起就可以作为点权使用。进行一次升序排序,就处理出一个完整的序列,每一个子任务应该交给连续的一段处理(这可以用临项交换证明),且由于子任务花费与大小有关,显然应该尽量平均,所以显然越靠前的段越短,这就算是一个优化。如果这样分段,理论上来说分成log段,就可以通过了。
DP部分:
for(i = 1;i <= b;i++) sum[i] = sum[i - 1] + dis[i];
dp[0][0] = 0;
for(i = 1;i <= b;i++){
dp[i][0] = 1e18;
for(j = 1;j <= min(i,s);j++){
dp[i][j] = 1e18;
for(k = i - i / j;k <= i;k++){
dp[i][j] = min(dp[i][j],dp[k][j - 1] + (sum[i] - sum[k]) * (i - k - 1));
}
}
}
printf("%lld\n",dp[b][s]);