愤怒的小N
题目描述
解法
首先可以发现奖励关就是二进制 \(1\) 个数为奇数的数。
先讲一下 \(60\) 分的做法,因为并不是人人一来就能拿满分,但这是正解的一个引子。
看到这个限制就想到了用数位 \(dp\) 去做,我们从小数位往大数位考虑,那么我们尝试维护 \(x^t\) 的和,每当遇到一个 1
之后就统计答案,不难发现可以固定大数位一定,这一位设置为 0
,然后小数位随便选,就可以统计出所有的数。
设 \(dp[t][0/1]\) 表示考虑了后 \(n-i\) 个小数位(我把这一维滚动掉了),\(1\) 的个数是偶数\(/\)奇数的 \(x^t\) 的和。因为这个状态的含义是乱填,转移就考虑这一位是填 \(0\) 还是 \(1\),原理就用二项式展开(其中 \(pre\) 表示之前的数):
\[(2^{n-i}+pre)^t=\sum_{l=0}^t pre^l\cdot 2^{(n-i)l}\cdot{t\choose l} \]那么我们就根据这个来转移:
-
这一位填 \(1\),\(dp[t][0]\leftarrow \sum_{l=0}^t 2^{(n-i)l}\cdot dp'[t-l][1]\cdot{t\choose l}\),\(dp[t][1]\leftarrow \sum_{l=0}^t2^{(n-i)l}\cdot dp'[t-l][0]\cdot{t\choose l}\)
-
这一位填 \(0\),\(dp[t][0]\leftarrow dp'[t][0]\),\(dp[t][1]\leftarrow dp'[t][1]\)
初始令 \(dp[0][0]=1\),算答案的时候也用二项式定理把前后合并起来即可,时间复杂度 \(O(k^2\log_2n)\),\(60\) 分到手。
然鹅我在考试的时候写错了一个小地方,调了很久,在我输出第二组样例的中间变量是发现了下面的神奇东西。
为了版面我就不贴过来了,点此观察数据(附暴力代码)
发现每迭代一次就会多一个 \(dp[t][0]\) 和 \(dp[t][1]\) 相等,迭代 \(k\) 次之后都满足 \(dp[t][0]=dp[t][1]\),我还以为是我暴力 \(dp\) 打错了,但是手玩了一下发现没问题,年轻的我不知道这就是正解的引子。
现在给出本题至关重要的结论,当迭代次数(\(dp\) 了多少次)大于 \(k\) 时,都有 \(dp[t][0]=dp[t][1]\),知道了这个结论可以归纳证明它:
当迭代次数为 \(1\) 时有 \(dp[0][0]=dp[0][1]=1\),当迭代次数为 \(x\) 的时候 \(dp'[x-1][0]+dp'[x-1][1]\) 会贡献到 \(dp[x-1][0],dp[x-1][1]\) 那里去,那么对于 \(x-1\) 这个次数的贡献是相等的,而比 \(x-1\) 更小的项由于数值相等的贡献是一样的,所以贡献相等,那么就多了 \(x-1\) 这一项的相等。
总结一句:秘密就藏在转移式里面。
那么也就是说迭代次数大于 \(k\) 之后答案就和 \(1\) 出现次数是奇是偶没关系了,直接算所有数除以 \(2\) 就得到奇数的答案了,那么可以改变一下算答案的方式,我们先统计出全部的数量:
\[\frac{1}{2}\sum_{i=0}^{n-1}f(i)=\frac{1}{2}\sum_{t=0}^{k-1}a_t\sum_{i=0}^{n-1}i^t \]\(\sum_{i=0}^{n-1}i^t\) 这个东西可以套路地算出来,就让 \(\tt yyb\) 教你算吧:点此学,这部分时间复杂度 \(O(\log_2 n+k^2)\)
然后还要用迭代次数小于等于 \(k\) 的答案去修正他,设 \(ji\) 表示 \(1\) 出现次数为奇数的答案,\(ou\) 表示 \(1\) 出现次数为偶数的答案,所以最终的答案就是:
\[\frac{1}{2}(\sum_{i=0}^{n-1}+ji-ou) \]然后 \(ji,ou\) 这两个东西可以用上面讲的暴力的做法 \(O(k^3)\) 算出来,总时间复杂度 \(O(\log_2 n+k^3)\)
积木小赛
题目描述
解法
唯一的教训是 \(\tt map\) 常数太大了,改成排序去重就过了。
时间复杂度 \(O(n^2\log n)\),考试的时候应该多关注一下常数问题吧。
#include <cstdio>
#include <algorithm>
using namespace std;
const int M = 3005;
#define ull unsigned 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,ans,dp[M][26];char s[M],t[M];
ull hs[M],pw[M],a[M*M];
ull get(int l,int r)
{
return hs[r]-hs[l-1]*pw[r-l+1];
}
int main()
{
n=read();
scanf("%s",s+1);scanf("%s",t+1);
//搞一点预处理
pw[0]=1;
for(int i=1;i<=n;i++)
{
pw[i]=pw[i-1]*391;
hs[i]=hs[i-1]*391+t[i];
}
for(int j=0;j<26;j++) dp[n+1][j]=n+1;
for(int i=n;i>=1;i--)
{
for(int j=0;j<26;j++)
dp[i][j]=dp[i+1][j];
dp[i][s[i]-'a']=i;
}
//枚举t的所有子串
for(int l=1;l<=n;l++)
{
int p=1;
for(int r=l;r<=n;r++)
{
//匹配不动了
if(dp[p][t[r]-'a']==n+1) break;
p=dp[p][t[r]-'a']+1;
a[++ans]=get(l,r);
}
}
sort(a+1,a+1+ans);
ans=unique(a+1,a+1+ans)-a-1;
printf("%d\n",ans);
}
岛屿探险
题目描述
解法
一开始写了一个树套树,虽然时间复杂度 \(O(n\log^2n)\) 但还是直接挂掉了,这东西又耗空间常数又大。
我们来分析一下要求的柿子:\(a_i\oplus c_j\leq \min(b_i,d_j)\),看到 \(\min\) 按照套路把它拆开。
如果要把他拆开就会产生一个关于 \(b_i\) 和 \(d_j\) 的偏序关系,解决偏序关系可以用 \(\tt cdq\) 分治,这个算法的套路是先想到分治再去想怎么套,所以我们把询问和修改塞到一起拿去 \(\tt cdq\),外层按 \(b_i,d_j\) 排序即可。
对于 \(b_i\leq d_j\) 的情况,问题转化成了 \(a_i\oplus c_j\leq b_i\),这就是左半边的修改对右半边询问的贡献,异或问题可以考虑 \(\tt tire\) 来解决,那么我们考虑一颗关于 \(c_j\) 的树,用 \(a_i,b_i\) 再上面跑的时候直接打永久化标记就行了,统计的时候就统计链上被打上的标记之和。
对于 \(b_i>d_j\) 的情况,问题转化成了 \(a_i\oplus c_j>d_j\),这就是右半边的修改对左半边询问的贡献,考虑一颗关于 \(a_i\) 的树,建出来之后把 \(c_j,d_j\) 在 \(\tt tire\) 树上面跑就行了。
然后左半边和右半边都按位置排序即可,询问拆成 \([1,l),[1,r]\) 这两个会方便一些。还有一个细节是 \(b_i=d_j\) 的情况不用关心,归类到哪一边算都可以,直接排序就行。时间复杂度 \(O(n\log^2n)\)
再写树套树我吃屎
#include <cstdio>
#include <algorithm>
using namespace std;
const int M = 300005;
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,m,q,cnt,rt,ans[M],ls[50*M],rs[50*M],s[50*M];
struct node
{
int x,c,d,f;
}a[M];
bool cmp1(node x,node y)
{
return x.d<y.d;
}
bool cmp2(node x,node y)
{
return x.x<y.x;
}
void init()//初始化
{
for(int i=1;i<=cnt;i++)
ls[i]=rs[i]=s[i]=0;
rt=cnt=0;
}
//subtask1
void ins1(int &x,int y,int p)
{
if(!x) x=++cnt;
s[x]++;
if(p==-1) return ;
if(y&(1<<p)) ins1(rs[x],y,p-1);
else ins1(ls[x],y,p-1);
}
int ask1(int x,int c,int d,int p)
{
if(!x || p==-1) return s[x];
if(d&(1<<p))
{
if(c&(1<<p)) return ask1(ls[x],c,d,p-1)+s[rs[x]];
return ask1(rs[x],c,d,p-1)+s[ls[x]];
}
if(c&(1<<p)) return ask1(rs[x],c,d,p-1);
return ask1(ls[x],c,d,p-1);
}
//subtask2
void tag(int &x)
{
if(!x) x=++cnt;
s[x]++;
}
void ins2(int &x,int c,int d,int p)
{
if(!x) x=++cnt;
if(p==-1) {tag(x);return ;}
if(d&(1<<p))
{
if(c&(1<<p)) {tag(rs[x]);ins2(ls[x],c,d,p-1);}
else {tag(ls[x]);ins2(rs[x],c,d,p-1);}
return ;
}
if(c&(1<<p)) ins2(rs[x],c,d,p-1);
else ins2(ls[x],c,d,p-1);
}
int ask2(int x,int c,int p)
{
int t=s[x];
if(!x || p==-1) return t;
if(c&(1<<p)) return t+ask2(rs[x],c,p-1);
return t+ask2(ls[x],c,p-1);
}
void cdq(int l,int r)
{
if(l==r) return ;
int mid=(l+r)>>1;
cdq(l,mid);cdq(mid+1,r);
sort(a+l,a+mid+1,cmp2);
sort(a+mid+1,a+r+1,cmp2);
//subtask1:右边建tire左边查
init();
for(int i=l,j=mid+1;i<=mid;i++)
if(a[i].f!=0)//是询问
{
while(j<=r && a[j].x<=a[i].x)
{
if(a[j].f==0) ins1(rt,a[j].c,23);
j++;
}
if(a[i].f>0) ans[a[i].f]+=ask1(rt,a[i].c,a[i].d,23);
else ans[-a[i].f]-=ask1(rt,a[i].c,a[i].d,23);
}
//subtask2:左边打标记右边查
init();
for(int i=mid+1,j=l;i<=r;i++)
if(a[i].f!=0)
{
while(j<=mid && a[j].x<=a[i].x)
{
if(a[j].f==0) ins2(rt,a[j].c,a[j].d,23);
j++;
}
if(a[i].f>0) ans[a[i].f]+=ask2(rt,a[i].c,23);
else ans[-a[i].f]-=ask2(rt,a[i].c,23);
}
}
int main()
{
n=read();q=read();
for(int i=1;i<=n;i++)
{
int c=read(),d=read();
a[++m]=node{i,c,d,0};
}
for(int i=1;i<=q;i++)
{
int l=read(),r=read(),c=read(),d=read();
a[++m]=node{r,c,d,i};
a[++m]=node{l-1,c,d,-i};
}
sort(a+1,a+1+m,cmp1);
cdq(1,m);
for(int i=1;i<=q;i++)
printf("%d\n",ans[i]);
}