前言
比赛之前我就想着先开 \(D\),然后肝了 \(1.8\) 个小时终于搞出来了,因为我是怂包所以不敢用大号交,用小号抢了 \(\tt Div2F\) 的首 \(A\)(好像赛时很少人做出来),就不想打了。
下次还是要相信自己的实力,自信即颠峰,\(3000\) 的题我不只切了一次两次了。不要畏惧难题,一发上红不是问题。
C. Extreme Extension
题目描述
定义操作为把数 \(a\) 拆成两个和等于 \(a\) 的正整数,定义序列的价值为最小的操作次数使之不降。
有 \(n\) 个数 \(a_i\),问每一个子区间的价值和,答案对 \(998244353\) 取模。
\(n\leq 10^5,a_i\leq 10^5\)
解法
首先考虑对于一个给定的区间如何计算价值。可以从后往前考虑,每一次都尽量大的拆分,这样的操作数是最小的,并且于后面也是最优的。设现在的开头是 \(x\),那么个数 \(k=\lceil\frac{a_i}{x}\rceil\),新的开头 \(x'=\lfloor\frac{a_i}{k}\rfloor\)
对于每个 \(i\),\(k\) 的取值只有 \(\sqrt n\) 种,那么 \(x'\) 的取值也只有 \(\sqrt n\) 种。因为计算只和开头的数字有关,可以把它当成状态记录下来,我们使用整体 \(dp\) 的技巧,也就是在所有右端点处初始化,在左端点处统计答案,只需要做一次 \(dp\) 就可以解决问题。
设 \(dp[i][j]\) 表示考虑到 \(i\) 开头的数字是 \(j\) 的方案数,时间复杂度 \(O(n\sqrt n)\)
总结
区间统计问题也可以考虑整体 \(dp\) 的技巧,考虑需要记录什么东西把统计问题转 \(dp\)
#include <cstdio>
#include <vector>
#include <iostream>
using namespace std;
const int M = 100005;
const int MOD = 998244353;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int T,n,w,ans,a[M],f[2][M];vector<int> v[2];
signed main()
{
T=read();
while(T--)
{
n=read();w=ans=0;
for(int i=1;i<=n;i++)
a[i]=read();
for(int i=n;i>=1;i--)
{
w^=1;int ls=a[i];
v[w].push_back(a[i]);
f[w][a[i]]=1;
for(auto x:v[w^1])
{
int k=(a[i]+x-1)/x,to=a[i]/k;
f[w][to]=(f[w][to]+f[w^1][x])%MOD;
ans=(ans+(k-1)*i*f[w^1][x])%MOD;
if(to^ls) v[w].push_back(to),ls=to;
}
for(auto x:v[w^1]) f[w^1][x]=0;
v[w^1].clear();
}
for(auto x:v[w]) f[w][x]=0;v[w].clear();
printf("%lld\n",ans);
}
}
D. Artistic Partition
题目描述
定义 \(c(l,r)\) 表示满足 \(l\leq i\leq j\leq r\) 并且 \(\gcd(i,j)\geq l\) 的 \((i,j)\) 对数。
定义 \(f(n,k)\) 为一个划分 \(0=x_1<x_2<...x_{k+1}=n\) 最小的 \(\sum_{i=1}^k c(x_i+1,x_{i+1})\)
多组数据,求 \(f(n,k)\)
\(T\leq 3\cdot 10^5,1\leq k\leq n\leq 10^5\)
解法
首先我想了一个 \(O(n^4k)\) 的 \(dp\),打了个表发现没什么规律,然后开始疯狂优化。
我发现 \(k\) 较小的时候答案就趋近 \(n\) 了,设左端点是 \(l\),那么 \([l,2l)\) 这一段的额外代价为 \(0\),所以说最多只需要 \(\tt log\) 次划分就可以使得答案达到最小值。
再考虑怎么优化枚举前驱的过程,可以盲猜一波有决策单调性,也就是满足下面的式子:
经我打表验证加上感性理解,可以证明上面的式子的成立的,那么套一个决策单调性分治即可。剩下的问题就是快速求 \(c(l,r)\) 了,比赛时我就是因为这个卡了很久,其实直接莫比乌斯反演即可:
所以求出欧拉函数的前缀和之后整除分块即可,在一段转移区间内求值的时候只需要把左端点拿出去做整除分块,剩下的可以略微修改推出来,时间复杂度我也不太清楚,因为是预处理所以该算法稳定在 \(1s\) 左右。
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int M = 100005;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int T,n,k,cnt,p[M],ph[M],dp[22][M];
void init(int n)
{
ph[1]=1;
for(int i=2;i<=n;i++)
{
if(!ph[i]) ph[i]=i-1,p[++cnt]=i;
for(int j=1;j<=cnt && i*p[j]<=n;j++)
{
if(i%p[j]==0)
{
ph[i*p[j]]=ph[i]*p[j];
break;
}
ph[i*p[j]]=ph[i]*(p[j]-1);
}
}
for(int i=1;i<=n;i++) ph[i]+=ph[i-1];
}
int cal(int m,int n)
{
int res=0;
for(int l=m,r=0;l<=n;l=r+1)
{
r=n/(n/l);
res+=ph[n/l]*(r-l+1);
}
return res;
}
void cdq(int l,int r,int L,int R)
{
if(l>r) return ;
int mid=(l+r)>>1,c=cal(L+1,mid),p=0;
for(int i=L;i<=min(R,mid);i++)
{
if(dp[k][mid]>dp[k-1][i]+c)
{
dp[k][mid]=dp[k-1][i]+c;
p=i;
}
c-=ph[mid/(i+1)];
}
cdq(l,mid-1,L,p);
cdq(mid+1,r,p,R);
}
signed main()
{
n=100000;init(n);
memset(dp,0x3f,sizeof dp);dp[0][0]=0;
for(k=1;k<=20;k++) cdq(1,n,0,n-1);
T=read();
while(T--)
{
n=read();k=read();
if(k>20) printf("%lld\n",n);
else printf("%lld\n",dp[k][n]);
}
}
E. A Perfect Problem
题目描述
定义一个序列是好的,当且仅当最大值乘上最小值大于等于权值和。
定义一个长度为 \(n\) 的序列 \(a\) 是完美的,当且仅当 \(1\leq a_i\leq n+1\),并且它的所有子序列都是好的。
给出 \(n\) 和质数 \(m\),求长度为 \(n\) 的完美序列 \(a\) 有模 \(m\) 下有多少个。
\(n\leq 200,10^8\leq m\leq 10^9\)
解法
真 \(\cdot\) 结论题,本题最关键的条件就是 \(1\leq a_i\leq n+1\)(这限制梦回 \(\tt NOI2020\))
\(\tt Observation\ one\):可以贪心转化判据,我们把 \(a\) 序列从小到大排序,如果最大值是 \(a_i\) 那么最小值一定是 \(a_1\),那么所有子序列合法就转化成了所有前驱合法。
\(\tt Observation\ two\):我们可以只考虑排序之后的序列,然后用简单组合数就可以计算出原序列,排序后序列需要满足 \(a_i\geq i\),否则 \(a_i\cdot a_1<i\cdot a_i\leq \sum_{j=1}^i a_j\) 显然不合法。
\(\tt Observation\ three\):如果 \(a_i=i\),那么 \(\forall j\leq i,a_j=i\),因为 \(a_i\cdot a_1=i\cdot a_1\leq \sum_{j=1}^i a_j\),所以当且仅当所有数等于 \(i\) 时取等,此时才能满足条件。
基于这个观察和对题设的分析,我们可以知道当 \(a_n=n\) 的时候唯一对应一种合法序列 ,所以可以假设 \(a_n=n+1\) 以方便下面的讨论。
\(\tt Observation\ four\):假设 \(a_n=n+1\),如果 \(a_i\geq i+1\),那么这个前缀自动合法。因为我们知道 \(a_1\cdot a_n\geq\sum_{i=1}^na_i\),所以 \(a_1\geq \sum_{i=1}^n a_i-a_1\),推出 \(a_1\geq \sum_{j=1}^i a_j-a_1\),所以前缀合法。
综合上文所有的观察,我们可以知道假设 \(a_n=n+1\),序列合法的充要条件是:
- \(\forall i\leq a_1,a_1\leq a_i\leq n+1\)
- \(\forall i>a_1,i+1\leq a_i\leq n+1\)
- \(\sum_{i=1}^n a_i-a_1\leq a_1\)
我们枚举 \(a_1\) 之后,可以去规划差值序列 \(b_i=a_i-a_1\),序列 \(b\) 合法的充要条件是:
- \(0\leq b_i\leq n+1-a_1\)
- \(\sum_{i=1}^n b_i\leq a_1\)
- 至少有 \(1\) 个数大于等于 \(n+1-a_1\),至少有 \(2\) 个数大于等于 \(n-a_1.....\)至少有 \(n-a_1\) 个数大于等于 \(2\)
设 \(dp[i][j][k]\) 表示考虑到权值 \(k\),已经在序列里放置了 \(i\) 个数,现在的总和是 \(j\),转移枚举新加数的权值有多少个,不难发现这是个调和级数,所以时间复杂度 \(O(n^4\log n)\)
\(\tt Observation\ seven\):有用的 \(a_1\) 只有后 \(2\sqrt n\) 种,所以时间复杂度 \(O(n^3\sqrt n\log n)\)
#include <cstdio>
#include <cstring>
#include <iostream>
#include <cassert>
using namespace std;
const int M = 205;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,MOD,a1,ans,fac[M],inv[M],f[M][M][M],v[M][M][M];
int qkpow(int a,int b)
{
int r=1;
while(b>0)
{
if(b&1) r=r*a%MOD;
a=a*a%MOD;
b>>=1;
}
return r;
}
int dfs(int i,int s,int k)
{
if(i==n) return fac[n];
if(k==0) return fac[n]*inv[n-i]%MOD;
if(v[i][s][k]==a1) return f[i][s][k];
int &r=f[i][s][k];r=0;v[i][s][k]=a1;
for(int j=(a1-s)/k;j>=0;j--)
{
if(k>1 && i+j<n-a1+1-k+1) continue;
r=(r+dfs(i+j,s+j*k,k-1)*inv[j])%MOD;
}
return r;
}
signed main()
{
n=read();MOD=read();fac[0]=1;
for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%MOD;
for(int i=0;i<=n;i++) inv[i]=qkpow(fac[i],MOD-2);
for(a1=max(1ll,n-30);a1<=n;a1++)
ans=(ans+dfs(0,0,n+1-a1))%MOD;
printf("%lld\n",ans);
}