邓明扬杂题选讲
stars
一颗星星可以抽象成 \(k\) 维空间中的一个整点。称若干星星构成的集合 \(S\) 是奇妙的,当且仅当存在 \(k\) 维空间中的整点 \(p\)(\(p\) 处可以有星星也可以没有),\(p\) 与 \(S\) 中的每颗星星至少有一维坐标相同。
有一个长度为 \(n\) 的星星序列 \(A\),请你求出所有奇妙子段的长度之和。
\(n \le 10^5\),\(k \le 5\) 。
考虑如何判定一个序列是奇妙的,由于 \(k\) 很小,我们可以枚举全排列,枚举 \(p\) 的哪个位置用于解决第一个点,然后往后推看有多少点顺带被解决了,遇到下一个不能解决的点再枚举 \(p\) 的一个位置。
不难发现只需要枚举长度为 \(k\) 的所有排列就可以判定合法性,但是暴力判定还是不可行,我们考虑用 \(dp\) 来优化这个过程,由于排列非常小我们可以直接记录在状态里。设 \(dp[i][s]\) 表示后 \(i\) 个点用位置排列 \(s\) 的最远延伸距离,考虑如何转移。
从右向左转移,维护每个维度失配的位置。
\(s\) 中的第一个位置会用来解决新增的左端点,剩下的是一个子问题,我们先用剩余的长为 \(k-1\) 的序列从 \(i+1\) 开始向后匹配,对于一个失配的位置,如果它可以被原来的第一个位置所解决,那么实际上在这一位是不会失配的,我们可以将原序列的第一个维度插入这个位置得到 \(s'\),\(dp[i][s]\) 可以从 \(dp[i+1][s']\) 转移。
时间复杂度 \(O(nk!k)\)。
[PA 2021] Od deski do deski
有 \(n\) 棵树,每棵树有可能是 \(m\) 种之一。
小 \(C\) 每天可以选择连续的一段树砍掉:要求这一段的长度至少是 \(2\),并且第一棵与最后一棵种类相同。
问有多少种初始局面,使得存在一种方法通过若干天将树砍光。对 \(10^9 + 7\) 取模。
\(n \le 3000\),\(m \le 10^9\)
考虑给定一个序列,如何判断合法,设 \(dp[i]\) 表示前缀 \([1,i]\) 是否合法,有转移 \(dp[i]|=dp[j] \ \ (j<i,a[i]=a[j-1])\) 。
当我们构造这个序列时,为了保证不重复统计,每次要选择最小的 \(j\) ,令 \(dp[i][j]\) 表示长度为 \(i\) ,有 \(j\) 个可供匹配的左端点的序列的数量,有:
时间复杂度 \(O(n^2)\) 。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m;
const int md=1e9+7;
int dp[3005][3005][2];
int main(){
scanf("%d%d",&n,&m);
dp[1][1][0]=m;
for(int i=1;i<n;i++){
for(int j=0;j<=i&&j<=m;j++){
dp[i+1][j][1]=(dp[i+1][j][1]+1ll*j*dp[i][j][0])%md;
dp[i+1][j][1]=(dp[i+1][j][1]+1ll*j*dp[i][j][1])%md;
dp[i+1][j][0]=(dp[i+1][j][0]+1ll*(m-j)*dp[i][j][0])%md;
dp[i+1][j+1][0]=(dp[i+1][j+1][0]+1ll*(m-j)*dp[i][j][1])%md;
}
}
int res=0;
for(int i=1;i<=n;i++)res=(res+dp[n][i][1])%md;
printf("%d\n",res);
return 0;
}
Guess
\(\text{Alice}\) 和 \(\text{Bob}\) 玩游戏。
\(\text{Alice}\) 有一个 \(1\) ~ \(n\) 中的正整数 \(y\)。\(\text{Bob}\) 不知道这个数。
游戏中的每一轮,\(\text{Bob}\) 选一个正整数 \(x\), 并提问 \(\text{Alice}\) : \(y\) 是否大于等于 \(x\)? 然后 \(\text{Alice}\) 需要回答是或否。
\(\text{Alice}\) 可以说至多一次谎。\(\text{Bob}\) 想要用尽量少的轮数确定 \(y\),\(\text{Alice}\) 则希望 \(\text{Bob}\) 用的次数尽量多。
假设双方除第一轮外均采用最优策略。你需要求出,对每个 \(x = 1, 2, ..., n\),当 \(\text{Bob}\) 第一轮问 \(y\) 是否大于等于 \(x\) 且 \(\text{Alice}\) 第一轮回答了是的情形下,\(\text{Bob}\) 还需要多少轮能确定 \(y\)。
\(2 \le n \le 2000\) 。
考虑每个位置被覆盖的次数,如果 \(\text{Bob}\) 问 是不是大于 \(x\),\(\text{Alice}\) 回答是,我们就把 \(\le x\) 覆盖一遍,否则把 \(> x\) 的覆盖一遍,如果一个数被覆盖了 \(\ge 2\) 次,就不可能是这个数。
\(\text{Bob}\) 赢:如果只有一个数覆盖次数 \(\le 1\) 。
转化问题为覆盖 \([1, 1, 1, 1, 2, 2, 1, 1]\) 和 \([1, 1, 1, 1, 1, 1]\) 。
注意到有效状态只有 \([1,1,1,1,0,0,0,0,1,1,1,1,1]\)
可以定义状态为 \(dp[a][b][c]\) 表示依次有 \(a\) 个 \(1\) \(b\) 个 \(0\) \(c\) 个 \(1\),还要问多少次 \([1,1,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1]\) 。
二分决策位置 \(O(n^3\log n)\),由于单调性:可以优化到 \(O(n^3)\) 。
进一步优化:注意到,答案是 \(\log\) 级别的,并且关于第三维是单调的。可以把值域和第三维互换,优化到 \(O(n^2\log n)\) 。
[PA2021 Round2 A] Poborcy podatkowi
有一棵 \(n\) 个点的树,边有边权(可能为负数)。
请你找出一些边不交的长为 \(4\) 的路径,使得权值之和最大。输出最大值。
\(n \le 5\times 10^5, -10^9 \le w[i] \le 10^9\) 。
暴力:\(dp[i][j]\) 表示 \(i\) 为根的子树,选择一些链长度都是 \(4\),然后以 \(i\) 为根有一条长度为 \(j\) 的链 这种情况下的最大权值和
转移:需要选一些子树 \(dp[v][j]\),满足:
-
\(j=1\) 和 \(j=3\) 需要一样多
-
\(j = 2\) 的个数需要是偶数
求最大权值之和:只需要记录当前的 \(1\) 的个数减 \(3\) 的个数,以及选择 \(2\) 的个数的奇偶性。
复杂度:\(n \times (\) 最大的的 \(1\) 的个数减 \(3\) 的个数\() \times 2\) 。
我们可以把儿子序列 \(\text{random_shuffle}\) 一下,第二维就可以限制在 \(\sqrt n\) 了。
Queue at the Bakery
你是一家面包店的老板,你有 \(m\) 个员工为顾客服务。 当一个顾客进入面包店时,他会排在其他正在等待的顾客后面。 只要有一个员工没有在为任何人服务,而至少有一个顾客在排队等候,就有一个员工立即开始为排队的第一个人服务(之后第一个人离开队伍)。
服务一个顾客正好需要 \(d\) 秒。 顾客的等待时间是从他进入面包店到有人开始为他服务之间所经过的秒数。
对于每个 \(i = 0, 1, …, n - 1\),有 \(p\) 的概率会有一个顾客在开店后刚好 \(i\) 秒进入面包店(这些事件独立)。 求所有顾客的总等待时间的期望值,输出实数,绝对或相对误差在 \(10^{-6}\) 以内即算正确。
\(n \le 5\times 10^4, m \le 10, d \le 10^3, 0.4 \le p \le 0.6\)
不难看出编号为 \(i\) 的服务员我们可以让他一定服务进来的编号 \(≡i\bmod m\),那么我们就可以对于每个服务员单独考虑,然后最后加起来即是答案。
考虑设 \(f_{i,j,k}\) 表示现在还有 \(i\) 个客户,该服务员还要继续工作 \(j\) 才能休息,当前客人模 \(m≡k\) 的期望等待时间。
不难发现,如果客人要进来,那么当 \(k=0\) 时,服务员就会去服务他,那么就会转移到 \(f_{i−1,j+d−1,m−1}\),贡献即为 \((f_{i−1,j+d−1,m−1}+j)×P\),如果 \(k≠0\),那么肯定不是我去服务,所以贡献即为 \(f_{i−1,j−1,k−1}×P\)。如果不进来,贡献即为 \(f_{i−1,j−1,k}×(1−P)\) 。
最后答案即为:
但是你发现复杂度有点离谱。注意到当 \(j\) 足够大的时候,那么服务员肯定是不间断服务,所以我让他前面多增加 \(t\) 时间,那么会增加的贡献就是 \(t×\) 他后面期望服务人数。所以我们就不需要处理每个 \(dp\),这个临界值差不多取到 \(1500\) 就可以了。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
int n,m,d,maxw=1500;
double pro,dp[2][1505][15],peo[50005][15];
inline double getit(int i,int j,int k){
return j<=maxw?dp[i&1][j][k]:(dp[i&1][maxw][k]+(j-maxw)*peo[i][k]);
}
int main(){
scanf("%d%d%d",&n,&m,&d);
scanf("%lf",&pro);
for(int i=1;i<=n;i++){
for(int j=0;j<=m-1;j++){
if(j==0)peo[i][j]=(peo[i-1][m-1]+1)*pro;
else peo[i][j]=peo[i-1][j-1]*pro;
peo[i][j]+=peo[i-1][j]*(1-pro);
}
}
for(int i=1;i<=n;i++){
for(int j=0;j<=maxw;j++){
for(int k=0;k<=m-1;k++){
if(k==0)dp[i&1][j][0]=(getit(i-1,j+d-1,m-1)+j)*pro;
else dp[i&1][j][k]=dp[i-1&1][j-!!j][k-1]*pro;
dp[i&1][j][k]+=dp[i-1&1][j-!!j][k]*(1-pro);
}
}
}
double ans=0;
for(int i=0;i<m;i++)ans+=dp[n&1][0][i];
printf("%.10f\n",ans);
return 0;
}
Scrupulous Robbery Plan
有 \(n\) 种物品,第 \(i\) 种质量为 \(i\),价格为 \(a_i\),每种物品的数量无限。给定 \(k,w\),请你选择 \(k\) 个物品,满足质量总和是 \(w\),价值之和最大,请求出最大的价值之和。
\(n≤1000,k≤10^6,k≤w≤kn,1≤a_i≤10^9\)
物品数量无限,需要选择的物品数较多,考虑倍增。由于物品大小不超过 \(n\) ,一定存在一种分割方案使得两边物品数量相等,质量之差不大于 \(n\),有:
我们考虑状态总数,因为 \(\dfrac{w}{2}\) 的操作,所以每一层的个数大概是 \(n+\dfrac{n}{2}+\dfrac{n}{4}....=O(n)\),一共有 \(O(\log k)\) 层。转移的复杂度是 \(O(n)\) 的,所以总复杂度 \(O(n^2\log k)\) 。
递归处理即可,使用 \(\text{map}\) 会超时,需要用数组,预先分配编号。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
long long dp[55][10005];
int n,k,w;
int a[1005],id[1000005],bas[1000005];
long long dfs(int K,int W){
if(!W)return K==0?0:-1e18;
if(!K)return -1e18;
// cout<<K<<" "<<id[K]<<" "<<W<<" "<<bas[id[K]]<<endl;
if(~dp[id[K]][W-bas[id[K]]+2500])return dp[id[K]][W-bas[id[K]]+2500];
if(K&1){
long long res=-1e18;
for(int i=1;i<=n&&i<=W;i++)res=max(res,dfs(K-1,W-i)+a[i]);
return dp[id[K]][W-bas[id[K]]+2500]=res;
}
int L=W/2,R=W-L;
long long res=-1e18;
for(int i=0;2*i<=n&&i<=L;i++)res=max(res,dfs(K/2,L-i)+dfs(K/2,R+i));
return dp[id[K]][W-bas[id[K]]+2500]=res;
}
int cnt;
int main(){
scanf("%d%d%d",&n,&k,&w);
memset(dp,-1,sizeof(dp));
for(int i=1;i<=n;i++)scanf("%d",&a[i]);bas[1]=w;
for(int i=k;i;){
id[i]=++cnt;
if(i&1)bas[cnt+1]=bas[cnt],i--;
else bas[cnt+1]=bas[cnt]/2,i/=2;
}
// for(int i=1;i<=cnt;i++)cout<<i<<" "<<bas[i]<<endl;
printf("%lld\n",dfs(k,w));
return 0;
}
[AGC044E]Random Pawn
圆上有 \(n\) 个点,你初始随机出生在一个点。
第 \(i\) 号点有属性 \(A[i], B[i]\)。如果你当前处在 \(i\) 号点,你可以选择结束游戏并获得 \(A[i]\) 元,或者花费 \(B[i]\) 元将自己随机移动到左右两点中的一个。
求最优策略的期望收益。收益为得到的钱减付出的钱。
输出实数,绝对或相对误差在 \(10^{-10}\) 以内即算正确。
\(n \le 2\times 10^5, 1 \le A[i] \le 10^{12}, 1 \le B[i] \le 100\)
显然有
从现实意义,或直接从式子来看,都会得知\(f_i\leqslant\max A_j\) 。所以最大的 \(A\) 值必然是直接作为 \(f_i\) 存在。
接下来,考虑次大的 \(f_i\) 吗?然而并不。用 \(\max A_j\) 作为端点,破环为链,考虑一个更简单的情况:
左式的含义是,直线。即,要么连成直线,要么落在 \((i,\omega_i)\) 点上;这其实就是上凸包。
那么,我们假设 \(g_i=f_i-c_i\) ,代入上式有:
对比系数,我们需要有 \(2c_i-c_{i-1}-c_{i+1}=2B_i\) ,是一个简单递推,随意规定初值即可。注意,现在已经破环为链了,\(c_1,c_{n+1}\) 不一定需要相同(因为这里不转移)。
时间复杂度 \(\mathcal O(n)\) 。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
int n;
long long a[200005],c[200005];
int b[200005];
struct node{
long long x,y;
inline node(long long _x=0,long long _y=0){x=_x,y=_y;}
inline node operator -(const node &t)const{
return node(x-t.x,y-t.y);
}
inline long long operator *(const node &b)const{
return x*b.y-y*b.x;
}
}d[200005];
inline void init(){
long long aa[200005],mx=-1;
int bb[200005],pos=0;
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%lld",&aa[i]);
if(mx<aa[i])pos=i,mx=aa[i];
}
for(int i=1;i<=n;i++)scanf("%d",&bb[i]);
memcpy(a+1,aa+pos,sizeof(long long)*(n-pos+1));
memcpy(a+n-pos+2,aa+1,sizeof(long long)*pos);
memcpy(b+1,bb+pos,sizeof(int)*(n-pos+1));
memcpy(b+n-pos+2,bb+1,sizeof(int)*pos);
}
int main(){
init();
c[1]=0;++n;
for(int i=2;i<=n;i++)c[i]=2*(c[i-1]+b[i-1])-c[i-2];
int h=0;
for(int i=1;i<=n;i++){
node t=node(i,a[i]-c[i]);
while(h>1&&(d[h]-d[h-1])*(t-d[h])>0)--h;
d[++h]=t;
}
long long ans=0;
for(int i=2;i<=h;i++){
ans+=(d[i].x-d[i-1].x)*(d[i].y+d[i-1].y);
}
ans+=d[1].y+d[h].y;
ans-=2*a[1];
for(int i=1;i<=n;i++)ans+=2*c[i];
printf("%.12lf",0.5*ans/(n-1.0));
return 0;
}
[AGC056E] Cheese
有一个长度为 \(n\) 的环,第 \(i+0.5(0≤i<n)\) 位置上各有一只老鼠。
然后进行以下操作 \(n−1\) 次:
第 \(i\) 个位置有 \(a_i\%\) 的概率被选择(选择一个),然后在这个位置上放一个奶酪,这个奶酪会顺时针跑,跑到一个老鼠的位置时有 \(50\%\) 的概率被吃掉,老鼠如果吃过奶酪就会离开。
求每只老鼠被留到最后的概率。
\(1≤n≤50,\sum^{n−1}_{i=0}a_i=100\) 。
考虑第 \(n\) 只老鼠留到最后的概率,因为无论是哪只奶酪吃什么老鼠都是一样的,所以我们可以考虑开始时就将所有的奶酪放下来,然后让它们一起绕着环走。
同样的,奶酪的移动顺序也是无关紧要的,所以我们可以考虑让所有的奶酪先绕道第 \(n\) 只老鼠的面前然后一起统计。
那么先考虑 \(dp\),设 \(f_{i,j,k}\) 表示现在已经放下的奶酪都已经走到了位置 \(i\) 处,然后已经放下了 \(j\) 个奶酪,已经有 \(k\) 个被老鼠吃了。
那么每次的转移分成两部分,第一部分是放奶酪,枚举这个位置放置了 \(x\) 个奶酪,需要注意的是除了概率以外因为这个方案是涉及可重排列的,所以还需要乘上一个 \(\dfrac{1}{x!}\)。
第二部分是吃奶酪,因为每只奶酪只会吃一个老鼠,所以统计没有这些奶酪都没有被吃的概率,很方便转移。
那么假设目前有 \(k\) 个奶酪走到了第 \(n\) 只老鼠前,那么说明前面还剩下 \(k−1\) 只老鼠没有被吃,那么考虑这种情况下第 \(n\) 只老鼠被留到最后的概率。
我们考虑绕一圈绕到第 \(i\) 只老鼠处还剩下 \(x\) 个奶酪,然后考虑 \(i\) 和 \(i+1\) 之间被留到最后的概率:
-
如果 \(i\) 和 \(i+1\) 都吃到了,我们显然得不出两个的概率关系。
-
如果 \(i\) 和 \(i+1\) 都没被吃到,我们也得不出来,考虑下一轮。
-
如果 \(i\) 吃了并且 \(i+1\) 没吃,概率是 \((1−\dfrac{1}{2^x})\dfrac{1}{2^{x−1}}\)
-
如果 \(i\) 没吃并且 \(i+1\) 吃了,概率是 \(\dfrac{1}{2^x}(1−\dfrac{1}{2^x})\)
那么记 \(p_i\) 表示 \(i\) 留到最后的概率,就有 \(p_i:p_{i+1}=\dfrac{1}{2^x}(1-\dfrac{1}{2^x}):(1-\dfrac{1}{2^x})\dfrac{1}{2^{x-1}}=1:2\)
然后又因为 \(\sum_{i=1}^{k-1}p_i=1\),所以有 \(p_i=\dfrac{2^{i-1}}{2^{k}-1}\),那么我们要求的就是 \(p_1=\dfrac{1}{2^k-1}\) 。
然后这样是求第 \(n\) 只老鼠的,改一下其他的 \(a_i\) 就好了。
时间复杂度:\(O(n^5)\) 。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int md=998244353;
int n,fac[45],inv[45],Pwr[45],iPwr[45];
inline void init(){
fac[0]=fac[1]=inv[0]=inv[1]=Pwr[0]=iPwr[0]=1;
for(int i=2;i<=n;i++)fac[i]=1ll*fac[i-1]*i%md;
for(int i=2;i<=n;i++)inv[i]=1ll*(md-md/i)*inv[md%i]%md;
for(int i=2;i<=n;i++)inv[i]=1ll*inv[i]*inv[i-1]%md;
for(int i=1;i<=n;i++)Pwr[i]=2ll*Pwr[i-1]%md;
for(int i=1;i<=n;i++)iPwr[i]=1ll*iPwr[i-1]*inv[2]%md;
}
int a[45],dp[45][45][45];
inline int pwr(int x,int y){
int res=1;
while(y){
if(y&1)res=1ll*res*x%md;
x=1ll*x*x%md;y>>=1;
}
return res;
}
int main(){
scanf("%d",&n);init();
for(int i=0;i<n;i++)scanf("%d",&a[i]);
for(int i=0;i<n;i++)a[i]=1ll*a[i]*pwr(100,md-2)%md;
for(int s=0;s<n;s++){
memset(dp,0,sizeof(dp));
dp[0][0][0]=1;
for(int x=1;x<=n;x++){
for(int i=0;i<n;i++){
for(int j=0;j<=i;j++){
for(int k=0,r=1;k<=i-j;k++,r=1ll*r*a[(s+x)%n]%md){
dp[x][i][j]=(dp[x][i][j]+1ll*dp[x-1][i-k][j]*r%md*inv[k])%md;
}
}
}
if(x!=n){
for(int i=0;i<n;i++){
for(int j=i-1;~j;j--){
dp[x][i][j+1]=(dp[x][i][j+1]+1ll*dp[x][i][j]*(1-iPwr[i-j]))%md;
dp[x][i][j]=1ll*dp[x][i][j]*iPwr[i-j]%md;
}
}
}
}
int res=0;
for(int i=1;i<=n;i++)res=(res+1ll*dp[n][n-1][n-i]*fac[n-1]%md*pwr(Pwr[i]-1,md-2))%md;
printf("%d ",res);
}
puts("");
return 0;
}