牛表
题目描述
给出三个整数 (x, y, P(1 ≤ x, y < P)),(P) 为素数。可以重复对 (x) 执行如下操作: 选择一个整数 (z ∈ [1, P − 1]),花费 (∣ x − z ∣) 的牛币,使得 (x = x cdot z mod P)。
最小需要花费多少牛币才能使得 (x = y)? 设 (ans(i, j)) 为 当 (x = i, y = j) 时的答案,为了减少输出,你需要输出 (sum_{i=1}^{P-1}sum_{i=1}^{P-1} ans(i,j)cdot t^{(i-1)cdot (P-1)+j-1}mod 998244353)。
(2le Ple 2000,1le tle 5000)。
解法
首先可以 (mathcal O(n^3)) 做 ( m Floyd)。打表可知,(ans(i,j)le 17),这就意味着边数不会超过 (35n)。用堆优化 ( m Dijkstra) 可以做到 (mathcal O(35n^2log n))。
不过复杂度可以优化到更优。
还是由于 (ans(i,j)le 17),所以边权范围很小,令其为 (D)。可以开 (D) 个队列,复杂度是 (mathcal O(n^2D)) 的。这完全等于没讲。
考虑 ( m Dijkstra) 需要重复入队和优先队列维护的原因是边权不同,可能原来更劣的解加上新的边权就更优了。开 (D) 个队列,哪种边就塞进哪个队列就规避了这个问题。具体更新过程:找到 (D) 个队列中当前离 (s)(源点)最近的点,枚举 (2D) 种边权来扩展,塞入对应边权的队列。
可以证明从队列中取出的点的最短路一定是递增的。可以用归纳法,先开始最短路是递增的,据此可以证明每个队列的值是递增的,反过来就可以证明原命题。
所以每个点只被更新一次(可以被入队多次),只用最优值来更新即可。每个点拓展一次,拓展复杂度 (mathcal O(D)),所以总共是 (mathcal O(n^2D)) 的。
在神仙学弟的代码上改了改:( m Link.)
代码
#include <cstdio>
#define print(x,y) write(x),putchar(y)
template <class T>
inline T read(const T sample) {
T x=0; char s; bool f=0;
while((s=getchar())>'9' or s<'0')
f |= (s=='-');
while(s>='0' and s<='9')
x = (x<<1)+(x<<3)+(s^48),
s = getchar();
return f?-x:x;
}
template <class T>
inline void write(const T x) {
if(x<0) {
putchar('-');
write(-x);
return;
}
if(x>9) write(x/10);
putchar(x%10^48);
}
#include <queue>
#include <iostream>
using namespace std;
const int inf = 5e6;
const int maxn = 2005;
const int mod = 998244353;
int p,t,head[maxn],cnt;
int dis[maxn];
bool vis[maxn];
struct edge {
int nxt,to,w;
} e[maxn*20];
struct node {
int u,w;
node() {}
node(int U,int W):u(U),w(W) {}
bool operator < (const node &t) const {
return w>t.w;
}
};
priority_queue <node> q;
inline int Abs(int x) {
return x>0?x:-x;
}
inline int inc(int x,int y) {
return x+y>=mod?x+y-mod:x+y;
}
void addEdge(int u,int v,int w) {
e[++cnt].to=v;
e[cnt].nxt=head[u];
e[cnt].w=w;
head[u]=cnt;
}
void Dijkstra(int x) {
for(int i=1;i<p;++i)
dis[i]=0x3f3f3f3f,vis[i]=0;
q.push(node(x,0));
dis[x]=0;
while(!q.empty()) {
node t=q.top(); q.pop();
if(vis[t.u] or (t.w^dis[t.u]))
continue;
vis[t.u]=1;
for(int i=head[t.u];i;i=e[i].nxt) {
int v=e[i].to;
if(dis[v]>dis[t.u]+e[i].w) {
dis[v]=dis[t.u]+e[i].w;
q.push(node(v,dis[v]));
}
}
}
}
int main() {
p=read(9),t=read(9);
for(int i=1;i<p;++i) {
int l=max(1,i-18);
int r=min(p-1,i+18);
for(int j=l;j<=r;++j)
addEdge(i,1ll*i*j%p,Abs(i-j));
}
int tmp=1,ans=0;
for(int i=1;i<p;++i) {
Dijkstra(i);
for(int j=1;j<p;++j)
ans = inc(ans,1ll*dis[j]*tmp%mod),
tmp = 1ll*tmp*t%mod;
}
print(ans,'
');
return 0;
}
矩阵学说
题目描述
给定 (n imes m) 的矩阵 (a)。求其中包含的恰好含 (k) 个不同整数个数的正方形。
(1le n,mle 1500,1le a_ile 100)。
解法
( ext{Subtask 1}):(nle 500)
枚举左上角和正方形边长。用两个 ( ext{long long}) 或 ( m bitset) 来维护边长增加时添加的数字。
( ext{Subtask 2}):(nle 1500)
对于固定的左上角,正方形包含数字种类是单调的嗄!二分边长,可以用二维 ( m st) 表预处理(( m bitset)),复杂度 (mathcal O left(n^2log ncdot frac{a_i}{w} ight))。
需要注意的是二维 ( m st) 表只适用于边长相等的情况。
代码
#pragma GCC optimize(2)
#pragma GCC optimize(Ofast)
#include <cstdio>
#define print(x,y) write(x),putchar(y)
template <class T>
inline T read(const T sample) {
T x=0; char s; bool f=0;
while((s=getchar())>'9' or s<'0')
f |= (s=='-');
while(s>='0' and s<='9')
x = (x<<1)+(x<<3)+(s^48),
s = getchar();
return f?-x:x;
}
template <class T>
inline void write(const T x) {
if(x<0) {
putchar('-');
write(-x);
return;
}
if(x>9) write(x/10);
putchar(x%10^48);
}
#include <bitset>
#include <iostream>
using namespace std;
const int maxn = 1505;
int n,m,K,lg[maxn],lim;
bitset <100> f[12][maxn][maxn];
void init() {
for(int k=1;k<=lg[lim];++k)
for(int i=1;i+(1<<k)-1<=n;++i)
for(int j=1;j+(1<<k)-1<=m;++j)
f[k][i][j] = (
f[k-1][i][j]|
f[k-1][i+(1<<k-1)][j]|
f[k-1][i][j+(1<<k-1)]|
f[k-1][i+(1<<k-1)][j+(1<<k-1)]
);
}
int ask(int u,int d,int l,int r) {
int dis=lg[r-l+1];
return (
f[dis][u][l]|
f[dis][u][r-(1<<dis)+1]|
f[dis][d-(1<<dis)+1][l]|
f[dis][d-(1<<dis)+1][r-(1<<dis)+1]
).count();
}
int calc(int x) {
int ret=0;
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j) {
int l=0,r=min(n-i,m-j)+1,mid;
while(l<r) {
mid=l+r+1>>1;
if(ask(i,i+mid-1,j,j+mid-1)<=x)
l=mid;
else r=mid-1;
}
ret += l;
}
return ret;
}
int main() {
n=read(9),m=read(9),K=read(9);
lim = max(n,m);
for(int i=2;i<=lim;++i)
lg[i]=lg[i>>1]+1;
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)
f[0][i][j][read(9)-1]=1;
init();
print(calc(K)-calc(K-1),'
');
return 0;
}
牛牛和数组操作
题目描述
给定长度为 (n) 的数列 (a)。你需要做 (n) 次操作,每次操作为以下形式:
- 选择一个整数 (x) 满足 (a_x ≠ 0),将 (a_x) 变成 (0),令 (l,r) 分别为离 (x) 向左/右最近的 (a) 为 (0) 的下标,此次操作的花费为 (max{a_l, a_{l+1}, . . . , a_{x−1}} + max{a_{x+1}, a_{x+2}, . . . , a_{r}}) 牛币。
问有多少不同的操作方式使得操作花费的牛币最少,两种操作不同当且仅当两种操作的操作序列不同。答案对 (998244353) 取模。
(1le nle 2000,1le a_ile n)。
解法
很难想象正解时间复杂度是 (mathcal O(n^3))。
首先可以发现,一次操作会将一个区间分成互不干扰的两半,这提醒我们区间 (mathtt{dp})。不过似乎我们并不需要 (mathtt{dp}) 得出最少花费,可以感性理解每次选择区间 (max) 作为划分点是最优的。
合并两个区间时贡献就是 (dp_{i,k-1}cdot dp_{k+1,j}cdot inom{j-i}{k-i}),就是两个长度为 (k-i,j-k) 的序列保留各自顺序进行合并。
另外讲一个小 (
m trick)?发现区间 (max) 如果相邻,它们的总贡献相当于是其中之一的贡献乘二。感觉没啥用。
代码
#pragma GCC optimize(2)
#include <cstdio>
#define print(x,y) write(x),putchar(y)
template <class T>
inline T read(const T sample) {
T x=0; char s; bool f=0;
while((s=getchar())>'9' or s<'0')
f |= (s=='-');
while(s>='0' and s<='9')
x = (x<<1)+(x<<3)+(s^48),
s = getchar();
return f?-x:x;
}
template <class T>
inline void write(const T x) {
if(x<0) {
putchar('-');
write(-x);
return;
}
if(x>9) write(x/10);
putchar(x%10^48);
}
const int maxn = 2005;
const int mod = 998244353;
int pos[maxn][maxn];
int n,a[maxn],dp[maxn][maxn];
int fac[maxn],ifac[maxn];
inline int inc(int x,int y) {
return x+y>=mod?x+y-mod:x+y;
}
inline int inv(int x,int y=mod-2) {
int r=1;
while(y) {
if(y&1) r=1ll*r*x%mod;
x=1ll*x*x%mod; y>>=1;
}
return r;
}
void init() {
fac[0]=1;
for(int i=1;i<=n;++i) {
pos[i][i]=i;
dp[i][i]=dp[i][i-1]=dp[i+1][i]=1;
for(int j=i+1;j<=n;++j)
if(a[pos[i][j-1]]<a[j])
pos[i][j]=j;
else pos[i][j]=pos[i][j-1];
fac[i]=1ll*fac[i-1]*i%mod;
}
ifac[n]=inv(fac[n]);
for(int i=n-1;i>=0;--i)
ifac[i]=1ll*ifac[i+1]*(i+1)%mod;
}
inline int C(int n,int m) {
if(n<m or n<0 or m<0)
return 0;
return 1ll*fac[n]*ifac[m]%mod*ifac[n-m]%mod;
}
int main() {
n=read(9);
for(int i=1;i<=n;++i)
a[i]=read(9);
init(); int F;
for(int len=2;len<=n;++len)
for(int i=1;i+len-1<=n;++i) {
int j=i+len-1,mx=a[pos[i][j]];
for(int k=pos[i][j];k<=j;k=pos[k+1][j]) {
if(a[k]^mx) break;
F = (k==i or k==j)?1:C(len-1,k-i);
dp[i][j] = inc(dp[i][j],1ll*dp[i][k-1]*dp[k+1][j]%mod*F%mod);
if(k==j) break;
}
}
print(dp[1][n],'
');
return 0;
与巨
题目描述
定义无穷序列 (f):(f_1=1,f_n=f_{n-1}cdot 2+1),函数 (G(x)=min_{f_ige x}f_i)。
定义 (dp_{c,0}=0),递推式:
求 (sum_{i=0}^n dp_{c,i}mod 998244353)。
(1le |n|le 10^7,1le cle 10^{18})。
解法
( ext{Subtask 1}):打表
对于 (|n|le 30,1le cle 50)。
有一个结论:(dp_{c,i}) 在 (c) 为偶数时恒为 (0)。这里感谢 ( ext{Emm_titan}) 送出的证明!
- (i=0)。此时值为零。
- (i eq 0)。找到 (i) 最低位的 (1) 的位置 (j),乘上一个偶数相当于 (j) 上的 (1) 出现了偶数次!所以第 (j) 位就变成 (0) 了,而且前面的位都不会更改它了。
因此就只有 (25) 个有用的 (c)。可以对 (dp_{c_i,kcdot 10^6}) 的前缀和进行打表。由于 (n) 在 (10^9) 范围,先找到离 (n) 最近的 (k),再 (mathcal O(10^6)) 地暴算。
( ext{Subtask 2}):非打表
我直呼 ( m gelivable)!非常地 ( m niubility)!
首先发现 (G(i)=2^{t+1}-1),(t) 为二进制最高位。那么 (x ext{ and }G(x)=xmod 2^{t+1})!
所以条件可以转化为 (icdot (c-1)mod 2^{t+1}=0)。将 (c-1) 表示为 (qcdot 2^p)((gcd(q,2)=1)),那么 (i) 必须被 (2^{t+1-p}) 整除。
由于 (t) 至多只到 (|n|-1),我们枚举 (t)。
-
(t<|n|-1)。
此时 ([2^t,2^{t+1})) 之内的数都在 (n) 之内,令 (g=t+1-p),那么 (i) 被 (2^g) 整除意味着 (i) 的低 (g) 位全为零。
那么满足条件的数实际上是 (2^t,2^t+2^g,...,2^t+kcdot 2^g)。其中 (k=2^{t-g})。而且每一项都贡献了 (2^g) 次,实际上对于 (2^t) 就是 ([2^t,2^t+2^g))。
-
(t=|n|-1)。
和上面类似,只是此时 (k=left lfloor frac{n}{2^g} ight floor+2^{t-g})。最后一项的贡献次数也多算了,需要减去。
代码
咕咕咕…