noip模拟5
T1
考试时间全砸在这题上了QAQ
结果没调出来线段树
40分很好拿,直接sort就可,打桶排也可,然而我打的挂了10分
100分做法:线段树
容易想到的是用线段树去维护区间内每个字母出现的次数,要开26颗线段树,常数还有点大。
所以换一种方法,用线段树去维护每个节点是否相等,相等的话,就返回区间大小。然后就是喜闻乐见的区间修改和区间查询。
其实桶排也可,但本人太弱,所以直接放blog
Code
#include<cstdio>
#define MAX 100001
#define re register
namespace OMA
{
int n,m;
int cnt[27];
char s[MAX];
struct Segment
{
struct TREE
{
int ch;
int l,r;
}tree[MAX<<2];
inline int ls(int now)
{ return now<<1; }
inline int rs(int now)
{ return now<<1|1; }
inline void Push_up(int now)
{ tree[now].ch = (tree[ls(now)].ch==tree[rs(now)].ch)?tree[ls(now)].ch:0; }
inline void Push_down(int now)
{ if(tree[now].ch){ tree[ls(now)].ch = tree[rs(now)].ch = tree[now].ch; } }
inline void build(int now,int l,int r)
{
tree[now].l = l,tree[now].r = r;
if(l==r)
{ tree[now].ch = s[l]-'a'+1; return ; }
int mid = (l+r)>>1;
build(ls(now),l,mid),build(rs(now),mid+1,r);
Push_up(now);
}
inline void query(int now,int l,int r)
{
if(l<=tree[now].l&&tree[now].r<=r&&tree[now].ch)
{ cnt[tree[now].ch] += tree[now].r-tree[now].l+1; return ; }
Push_down(now);
int mid = (tree[now].l+tree[now].r)>>1;
if(l<=mid)
{ query(ls(now),l,r); }
if(r>mid)
{ query(rs(now),l,r); }
}
inline void update(int now,int l,int r,int pos)
{
if(l<=tree[now].l&&tree[now].r<=r)
{ tree[now].ch = pos; return ; }
Push_down(now);
int mid = (tree[now].l+tree[now].r)>>1;
if(l<=mid)
{ update(ls(now),l,r,pos); }
if(r>mid)
{ update(rs(now),l,r,pos); }
Push_up(now);
}
inline void print(int now)
{
if(tree[now].ch)
{
for(re int i=tree[now].l; i<=tree[now].r; i++)
{ printf("%c",tree[now].ch+'a'-1); }
return ;
}
print(ls(now)),print(rs(now));
}
}Tree;
inline int read()
{
int s=0,w=1; char ch=getchar();
while(ch<'0'||ch>'9'){ if(ch=='-')w=-1; ch=getchar(); }
while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar(); }
return s*w;
}
signed main()
{
n = read(),m = read();
scanf("%s",s+1);
Tree.build(1,1,n);
for(re int i=1; i<=m; i++)
{
int l = read(),r = read(),x = read();
int temp = l;
for(re int j=1; j<=26; j++)
{ cnt[j] = 0; }
Tree.query(1,l,r);
if(x)
{
for(re int j=1; j<=26; j++)
{
if(cnt[j])
{ Tree.update(1,temp,temp+cnt[j]-1,j); temp += cnt[j]; }
}
}
if(!x)
{
for(re int j=26; j>=1; j--)
{
if(cnt[j])
{ Tree.update(1,temp,temp+cnt[j]-1,j); temp += cnt[j]; }
}
}
}
Tree.print(1);
return 0;
}
}
signed main()
{ return OMA::main(); }
T2
神仙dp题
下午letitdown讲题时说,把矩阵倒过来,给我听傻了
直接状压可拿20分
100分做法:
首先要明白,左右区间在那一列是没有区别,我们要的是在i点及其之前有多少左区间端点和多少右区间端点,我们分别用 \(l_{i}\) 和 \(r_{i}\) 来表示。
接下来考虑dp,设 \(dp_{i,j}\) 表示前i列中有j个1放在右区间中,考虑以下几种情况:
当我们dp到一个新的状态,即一个新的 \(i,j\),我们需要去维护该状态是否合法。
\(i-j-l_{i-1}\) 为左区间最多能放的个数,\(l_{i}-l_{i-1}\) 为在区间中必须要放的个数,显然,\(A_{n}^{m}\) 如果 \(m>n\) 会返回0,即可判定此状态非法,同时该状态对其他状态的贡献也为0
另一种解释,还是dp到了一个新的状态,我们开始考虑左区间,考虑什么样的左区间? 因为我们在i之前的已经考虑过了,而在i之后的现在还不用去考虑,所以我们只需要去考虑端点在i上的左区间即可,那么则需要在前i列里选出满足还没放1,同时端点又在第i列的左区间去放1,前者即为 \(i-j-l_{i-1}\),后者即为 \(l_{i}-l_{i-1}\)
空着不放1时,直接转移即可
当放在右区间中时,在前i+1列,则有 \(r_{i+1}-j\) 个区间没有放1,则
关于第一个方程中为什么是排列而不是组合,请参考此篇blog
Code
#include<cstdio>
#define MAX 3010
#define re register
#define int long long
namespace OMA
{
int n,m;
int dp[MAX][MAX];
int l[MAX],r[MAX];
int c[MAX],inv[MAX];
const int p=998244353;
inline int read()
{
int s=0,w=1; char ch=getchar();
while(ch<'0'||ch>'9'){ if(ch=='-')w=-1; ch=getchar(); }
while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar(); }
return s*w;
}
inline int quickpow(int a,int b)
{
int ans = 1;
while(b)
{
if(b&1)
{ ans = ans*a%p; }
a = a*a%p;
b >>= 1;
}
return ans;
}
void begin()
{
c[0] = inv[0] = 1;
for(re int i=1; i<=m; i++)
{ c[i] = i*c[i-1]%p; }
inv[m] = quickpow(c[m],p-2);
for(re int i=m-1; i>=1; i--)
{ inv[i] = (i+1)*inv[i+1]%p; }
}
inline int A(int n,int m)
{ return c[n]*inv[n-m]%p; }
signed main()
{
n=read(),m=read();
begin();
for(re int i=1; i<=n; i++)
{ l[read()]++,r[read()]++; }
for(re int i=1; i<=m; i++)
{ l[i] += l[i-1],r[i] += r[i-1]; }
dp[0][0] = 1;
for(re int i=0; i<=m; i++)
{
for(re int j=0; j<=i; j++)
{
if(i)
{ dp[i][j] = (dp[i][j]*A(i-j-l[i-1],l[i]-l[i-1]))%p; }
dp[i+1][j] = (dp[i+1][j]+dp[i][j])%p;
dp[i+1][j+1] = (dp[i+1][j+1]+dp[i][j]*(r[i+1]-j))%p;
}
}
printf("%lld\n",dp[m][n]);
return 0;
}
}
signed main()
{ return OMA::main(); }
T3
先%一波yspm
难点在于题目中所给式子的转换,先看题目中的式子 \(x=\left(\left\lfloor\frac{2x}{2^{n}}\right\rfloor+2x\right)\mod2^{n}\) 其实就是逻辑左移然而我并没有看出来
\(\left\lfloor\frac{2x}{2^{n}}\right\rfloor\) 即 \(x\gg\left(n-1\right)\) 取第n位,加上 \(2x\) 即 \(x\ll1\) 将x左移1位,后再模一下 \(2^{n}\) 就是取后n位,所以啊,这波操作,就是将x二进制的最高位移至最低位。
设 \(pre_{i}\) 为操作后的前缀异或和,\(suf_{i}\) 为给的 \(a_{i}\) 的后缀异或和,所以对手给你操作后,将相当于i将你的x异或了个 \(pre_{i}\oplus suf_{i}\),一共有m+1个这玩意。
然后就是喜闻乐见的01trie,从根节点开始dfs遍历
则会有以下三种情况:
该节点有两个子节点,即0和1,那么你无论选那个,对手都会给你来一波操作,让你变成0,此时对答案没有贡献,直接向下dfs
该节点只有一个子节点,则考虑其对答案的贡献,无论是0还是1,贡献都为 \(2^{n-dep-1}\),\(dep\) 为遍历到现在的深度
该节点为叶子节点,那就该更新答案了,如果与记录的最大值相等,那就给数量+1,如果比记录的最大值要大,就更新最大值,同时将答案数量要重置为1
Code
#include<cstdio>
#define MAX 100001
#define re register
namespace OMA
{
int n,m;
int ans1,ans2;
int a[MAX],bin[MAX]={1};
int x[MAX],pre[MAX],suf[MAX];
struct Trie
{
int tot;
int ch[MAX*31][2];
inline void insert(int x)
{
int u = 0;
for(re int i=n-1; i>=0; i--)
{
int pos = (x>>i)&1;
if(!ch[u][pos])
{ ch[u][pos] = ++tot; }
u = ch[u][pos];
}
}
inline void dfs(int u,int ans,int dep)
{
if(ch[u][0]&&ch[u][1])
{ dfs(ch[u][0],ans,dep+1),dfs(ch[u][1],ans,dep+1); return ; }
if(!ch[u][0]&&!ch[u][1])
{
ans2 += (ans==ans1)?1:0;
if(ans>ans1)
{ ans1 = ans, ans2 = 1; }
return ;
}
if(!ch[u][0]||!ch[u][1])
{ dfs(ch[u][0]|ch[u][1],ans+bin[n-dep-1],dep+1); return ; }
}
}tree;
inline int read()
{
int s=0,w=1; char ch=getchar();
while(ch<'0'||ch>'9'){ if(ch=='-')w=-1; ch=getchar(); }
while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar(); }
return s*w;
}
signed main()
{
n=read(),m=read();
for(re int i=1; i<=n; i++)
{ bin[i] = bin[i-1]<<1; }
for(re int i=1; i<=m; i++)
{ x[i] = (2*(a[i] = read())/bin[n]+2*a[i])%bin[n]; }
for(re int i=1; i<=m; i++)
{ pre[i] = pre[i-1]^x[i]; }
for(re int i=m; i>=1; i--)
{ suf[i] = suf[i+1]^a[i]; }
for(re int i=0; i<=m; i++)
{ tree.insert(pre[i]^suf[i+1]); }
//for(re int i=0; i<=m; i++)
//{ printf("%d ",pre[i]^suf[i+1]); }
tree.dfs(0,0,0);
printf("%d\n%d\n",ans1,ans2);
return 0;
}
}
signed main()
{ return OMA::main(); }
T4
咕咕咕