P2709
Description
给定一个序列, q q q次询问一个区间中每个数出现次数的平方和。
Solution
考虑莫队。
将一个数加入进时,假设这个数是 k k k, f i f_i fi表示当前维护的序列中 i i i出现的次数,显然加入后 f k f_k fk的值增加了 1 1 1,则答案增加了 ( f k + 1 ) 2 − f k 2 = 2 f k + 1 (f_{k}+1)^2-f_k^2=2f_k+1 (fk+1)2−fk2=2fk+1。删去数的操作同理。
于是,我们直接在分块式排序后,每次移动左右端点且同时维护答案即可。时间复杂度 O ( n n ) O(n \sqrt n) O(nn)。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n,m,k,blo,ans=0;
int a[50005],v[50005],al[50005],p[50005];
struct node
{
int rt;
int l,r;
}q[50005];
bool cmp(node x,node y)
{
if (al[x.l]!=al[y.l]) return al[x.l]<al[y.l];
else return x.r<y.r;
}
inline void get_insert(int ii)
{
ans=ans+2*v[a[ii]]+1;
v[a[ii]]++;
}
inline void out(int ii)
{
ans=ans-2*v[a[ii]]+1;
v[a[ii]]--;
}
signed main()
{
cin>>n>>m>>k;
blo=sqrt(n);
for (int i=1;i<=n;i++) cin>>a[i];
for (int i=1;i<=n;i++) al[i]=(i-1)/blo+1;
for (int i=1;i<=m;i++)
{
cin>>q[i].l>>q[i].r;
q[i].rt=i;
}
sort(q+1,q+m+1,cmp);
for (int i=1;i<=m;i++)
{
if (al[q[i].l]!=al[q[i-1].l])
{
for (int j=1;j<=k;j++) v[j]=0;
ans=0;
for (int j=q[i].l;j<=q[i].r;j++) v[a[j]]++;
for (int j=1;j<=k;j++) ans+=v[j]*v[j];
}
else
{
for (int j=q[i-1].r+1;j<=q[i].r;j++) get_insert(j);
if (q[i-1].l<q[i].l)
{
for (int j=q[i-1].l;j<q[i].l;j++) out(j);
}
else
{
for (int j=q[i-1].l-1;j>=q[i].l;j--) get_insert(j);
}
}
p[q[i].rt]=ans;
}
for (int i=1;i<=n;i++) cout<<p[i]<<endl;
cout<<endl;
return 0;
}
CF982C
Description
求最多能删除给定树的多少条边,使得剩下的森林中每棵树的大小均为偶数。
Solution
做法显然。
对于所有子树(包括它自己)大小为偶数的子树均可以单独成为一个森林,即可以删去从这棵子树的根连向它父亲的一条边。我们可以贪心地多删边,即答案为子树大小为偶数的子树数量。
一遍 d f s dfs dfs即可求出各个子树的大小 s i z e size size,时间复杂度 O ( n ) O(n) O(n)。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
int t,n,k=2,cnt=0,ans=0,u,v;
int head[200005],size[200005];
struct edge
{
int next;
int to;
}e[200005];
inline void add_edge(int u,int v)
{
cnt++;
e[cnt].to=v;
e[cnt].next=head[u];
head[u]=cnt;
}
inline void dfs(int now,int fath)
{
size[now]=1;
for (int i=head[now];i;i=e[i].next)
{
if (e[i].to!=fath)
{
dfs(e[i].to,now);
size[now]+=size[e[i].to];
}
}
}
signed main()
{
cin>>n;
for (int i=1;i<n;i++)
{
cin>>u>>v;
add_edge(u,v);
add_edge(v,u);
}
if (n%k!=0) return puts("-1"),0;
dfs(1,0);
for (int i=1;i<=n;i++)
{
if (size[i]%2==0) ans++;
}
cout<<ans-1<<endl;
return 0;
}
CF682C
Description
如果一个节点 x x x的后代中,存在 y y y使得 x x x到 y y y的路径上经过的节点的点权和大于 y y y自己的点权和,则称 x x x节点是不开心的。每次可以删去一个叶节点,求最少需要多少次删除才能使得剩下的节点中不存在不开心的节点。
Solution
若从某个 x x x的祖先节点到 x x x经过的所有点的点权之和大于 x x x的点权,则显然这棵树不满足要求。
于是,我们可以维护一个值,即从某个节点到 x x x的点权之和的最大值,从而直接判断该树是否满足要求。设当前这个最大值为 t t t,连到子节点的边的权值为 w w w,马上要深搜到的子节点的这个最大值为 n o w now now,则有
①当
t
<
0
t<0
t<0时,
n
o
w
=
w
now=w
now=w
②当
t
≥
0
t≥0
t≥0时,
n
o
w
=
w
+
t
now=w+t
now=w+t
注意这个思路与"最大子段和"的解法相似,可以参考。
若不满足要求,应该删去以该节点为根的整棵子树,所以要预处理出每个节点的子树大小。然后线性用上述方法扫描即可在 O ( n ) O(n) O(n)的代价下得到答案。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n,cnt=0,ans=0,u,v,w;
int head[100005],a[100005],size[100005],tot[100005];
struct edge
{
int next;
int to;
int dis;
}e[200005];
inline void add_edge(int u,int v,int w)
{
cnt++;
e[cnt].to=v;
e[cnt].dis=w;
e[cnt].next=head[u];
head[u]=cnt;
}
inline void dfs(int now,int fath)
{
size[now]=1;
for (int i=head[now];i;i=e[i].next)
{
if (e[i].to!=fath)
{
dfs(e[i].to,now);
size[now]+=size[e[i].to];
}
}
}
inline void dfs2(int now,int fath,int nowdis)
{
if (nowdis>a[now])
{
ans+=size[now];
return;
}
for (int i=head[now];i;i=e[i].next)
{
if (e[i].to!=fath)
{
if (nowdis>=0) dfs2(e[i].to,now,nowdis+e[i].dis);
else dfs2(e[i].to,now,e[i].dis);
}
}
}
signed main()
{
cin>>n;
for (int i=1;i<=n;i++) cin>>a[i];
for (int i=1;i<n;i++)
{
cin>>u>>w;
add_edge(u,i+1,w);
add_edge(i+1,u,w);
}
dfs(1,0);
dfs2(1,0,0);
cout<<ans<<endl;
return 0;
}
CF959C
Description
对于一个错误的算法(详见题面),请举出它的一个 H a c k Hack Hack数据与非 H a c k Hack Hack数据。
Solution
巧妙的构造题, C F CF CF的代表。
Hack数据
若 n ≤ 5 n≤5 n≤5,则不存在;
否则存在 H a c k Hack Hack数据。这棵树是这样的:根节点连向 n − 3 n-3 n−3个子节点,其中一个子节点另有两个孩子。
为什么 H a c k Hack Hack了呢?可以发现,删去根节点再删去那个有两个孩子的节点, 2 2 2步即可完成要求。而按照 M a h m o u d Mahmoud Mahmoud说的,这需要 3 3 3步才可以,显然错误。
非Hack数据
非 H a c k Hack Hack数据总存在,构造一条长度为 n n n的链即可。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n;
signed main()
{
cin>>n;
if (n<=5) cout<<-1<<endl;
else
{
for (int i=2;i<=n-2;i++) cout<<1<<' '<<i<<endl;
cout<<2<<' '<<n-1<<endl;
cout<<2<<' '<<n<<endl;
}
for (int i=1;i<n;i++) cout<<i<<' '<<i+1<<endl;
return 0;
}
P5684
Description
将一个字符串重排,求有多少种排列方式使得它不是一个回文串。
Solution
显然应从反面考虑,然后分步骤考虑。
首先,检验该字符串能否成为一个回文串。如果可以,则判断,这个字符串的长度的奇偶性。如果长度为奇数,那么我们找出唯一一个出现次数为奇数的字符出现的次数为 k k k,接着删去一个这样的字符, k : = k − 1 k:=k-1 k:=k−1,同时最终答案也要乘上 k k k。
现在,该字符串就是一个能够成为回文字符串且长度为偶数的可爱的字符串啦~现在考虑方案数。
①配对
定义一个长度为
n
n
n的回文串的第
i
i
i位与第
n
+
1
−
i
n+1-i
n+1−i位是配对的,显然这两个位置的字符要相同。所以第一步,我们要在原字符串中找相同的数进行配对。
对于同一种字符,假设有 x x x个,那么可以配成 C x 2 × C x − 2 2 × C x − 4 2 … … × 1 C_{x}^2×C_{x-2}^2×C_{x-4}^2……×1 Cx2×Cx−22×Cx−42……×1对,但是还有重复,即 ( 1 , 4 ) ( 2 , 3 ) (1,4)(2,3) (1,4)(2,3)与 ( 2 , 3 ) ( 1 , 4 ) (2,3)(1,4) (2,3)(1,4)是重复的,所以还要除以 ( x / 2 ) ! (x/2)! (x/2)!的值。累乘上每种相同字符配对的方案数。
②大选位
现在有
n
/
2
n/2
n/2对,我们要安排它们的顺序,显然有
(
n
/
2
)
!
(n/2)!
(n/2)!种。不可行的方案数要乘上它。
③小选位
对于每对,均有两种安排方式,即若一对的下标为
1
,
3
1,3
1,3,可以有
1
,
3
1,3
1,3和
3
,
1
3,1
3,1两种方式。即不可行的方案数还要乘上
2
n
/
2
2^{n/2}
2n/2。
于是,最终答案为 n ! n! n!减去不可行的方案数,注意除法用逆元即可。
时间复杂度 O ( n ) O(n) O(n)。
作为普及组赛题,逆元是不是超纲了呀(逃
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int mod=1e9+7;
int n,flag=0,out=1,ans=1,f[1005],jc[2005];
char x;
int quick_power(int a,int b)
{
int res=1;
for (;b;b=b>>1,a=(a*a)%mod)
{
if (b&1) res=(res*a)%mod;
}
return res;
}
int ny(int k)
{
return quick_power(k,mod-2);
}
int solve(int k)
{
if (k==0) return 1;
int tot=1;
for (int i=k;i>=2;i-=2) tot=(tot*(((i*i-i)/2ll)%mod))%mod;
return (tot*ny(jc[k/2]))%mod;
}
inline void init()
{
jc[0]=1;
for (int i=1;i<=2000;i++) jc[i]=(jc[i-1]*i)%mod;
}
signed main()
{
cin>>n;
init();
for (int i=1;i<=n;i++)
{
cin>>x;
f[(int)(x)]++;
}
for (int i=1;i<=300;i++)
{
if (f[i]%2==1)
{
out=f[i];
f[i]--;
flag++;
}
}
if (flag>1) ans=0;
else
{
for (int i=1;i<=300;i++) ans=(ans*solve(f[i]))%mod;
ans=(ans*jc[n/2])%mod;
ans=(ans*quick_power(2,n/2))%mod;
}
cout<<((jc[n]-((ans*out)%mod))%mod+mod)%mod<<endl;
return 0;
}
P5683
Description
请拆除最多的道路,使得从 1 1 1到 s 1 s1 s1的最短路径不超过 t 1 t1 t1且 1 1 1到 s 2 s2 s2的最短路径不超过 t 2 t2 t2。
Solution
我们最终留下的图肯定是这样的:
存在一个节点 s s s, 1 1 1到 s s s一条蜿蜒的路径, s s s到 s 1 , s 2 s1,s2 s1,s2又有两条路径,其他的没有
设 1 1 1到 s s s的最短路径长度为 k 1 k1 k1, s s s到 s 1 s1 s1的最短路径长度为 k 2 k2 k2, s s s到 s 2 s2 s2的最短路径长度为 k 3 k3 k3。
于是,我们直接枚举 s s s,检验是否满足 k 1 + k 2 ≤ t 1 k1+k2≤t1 k1+k2≤t1且 k 1 + k 3 ≤ t 2 k1+k3≤t2 k1+k3≤t2。注意,我们要预处理出以 1 , s 1 , s 2 1,s1,s2 1,s1,s2为源点的单源最短路径,后面枚举时的 k 1 , k 2 , k 3 k1,k2,k3 k1,k2,k3均可以 O ( 1 ) O(1) O(1)查询。
时间复杂度 O ( ( n + m ) l o g 2 n ) O((n+m)log_2n) O((n+m)log2n)。
Code
#include <bits/stdc++.h>
#define int long long
#define inf 2000000007
using namespace std;
int n,m,cnt=0,u,v,s1,t1,s2,t2,ans=inf;
int head[200005],dis[5][200005],visited[200005];
struct edge
{
int next;
int to;
int dis;
}e[200005];
struct node
{
int dis;
int pos;
bool operator < (const node &x) const
{
return x.dis<dis;
}
};
std::priority_queue<node> q;
inline void add_edge(int u,int v,int w)
{
cnt++;
e[cnt].to=v;
e[cnt].dis=w;
e[cnt].next=head[u];
head[u]=cnt;
}
inline void dijkstra(int t,int s)
{
dis[t][s]=0;
q.push((node){0ll,s});
while (!q.empty())
{
node tmp=q.top();
q.pop();
int x=tmp.pos;
if (visited[x]==1) continue;
visited[x]=0;
for (int i=head[x];i;i=e[i].next)
{
int y=e[i].to;
if (dis[t][y]>dis[t][x]+e[i].dis)
{
dis[t][y]=dis[t][x]+e[i].dis;
if (!visited[y]) q.push((node){dis[t][y],y});
}
}
}
}
inline void clear()
{
for (int i=1;i<=n;i++) visited[i]=0;
}
signed main()
{
cin>>n>>m;
for (int i=1;i<=m;i++)
{
cin>>u>>v;
add_edge(u,v,1);
add_edge(v,u,1);
}
cin>>s1>>t1>>s2>>t2;
for (int i=1;i<=3;i++)
{
for (int j=1;j<=n;j++) dis[i][j]=inf;
}
dijkstra(2,s1),clear();
dijkstra(1,1),clear();
dijkstra(3,s2),clear();
for (int i=1;i<=n;i++)
{
int first=dis[1][i]+dis[2][i];
int second=dis[1][i]+dis[3][i];
if (first<=t1&&second<=t2) ans=min(ans,dis[1][i]+dis[2][i]+dis[3][i]);
}
if (ans==inf) cout<<-1<<endl;
else cout<<m-ans<<endl;
return 0;
}
P6015
Description
有一个牌堆,一共有 n n n张牌,第 i i i张牌上有一个数 a i a_i ai,其中第一张牌是堆顶。
小 Z 先取牌,他可以从堆顶开始取连续若干张牌(可以取 0 0 0张),取完的牌拿在手上,也就是不在牌堆里了。
然后小Y取牌,同样,她也可以从堆顶开始取连续若干张牌(可以取 0 0 0张)。
如果一个人手上的牌的数字和大于 X X X,那么他的分数就是 0 0 0,否则分数就是数字和。
分数高的人获胜,如果一样高,则无人获胜。
小Z为了获胜,使用了透视挂,即他知道牌堆里每张牌上写的数。
现在问你对于满足 1 ≤ X ≤ K 1 \leq X \leq K 1≤X≤K 的所有整数 X X X,哪些可以使得小 Z 有必胜策略,即小Z取完后,不管小Y怎么取都一定会输。
Solution
本题本蒟蒻的做法十分特别。
首先,对于一个长度为 k k k的前缀和为 p r e k pre_k prek,小 X X X取到了 k k k但是没有取 k + 1 k+1 k+1,说明有 p r e k ≤ X < p r e k + 1 pre_k≤X<pre_{k+1} prek≤X<prek+1。
同时,维护小 Y Y Y此时取的牌之和,最佳策略下她取了总和为 T T T的许多牌,其中 T T T是最小的一个 T T T使得 T ≥ p r e k T≥pre_k T≥prek,且 T T T可以得到。
此时, X X X的取值必须在 p r e k pre_k prek到 m i n ( p r e k + 1 , T − 1 ) min(pre_{k+1},T-1) min(prek+1,T−1)之间,否则小 Z Z Z无法获胜。对于这样的区间修改(标记 [ p r e k , m i n ( p r e k + 1 , T − 1 ) ] [pre_k,min(pre_{k+1},T-1)] [prek,min(prek+1,T−1)]这个区间内的 X X X均可以使小 Y Y Y有必胜策略),可以用离线差分来完成。
注意在前缀和预处理后,用双指针做法来维护小 Y Y Y最多能取到第几张牌,小 Z Z Z能取到第几张牌(后者很易维护)。由于这两个指针的移动方向都是单调的,故时间复杂度为 O ( n ) O(n) O(n)。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxlen=1000000;
int n,k,ii=0,jj=0,tot=0,pos=0;
int a[maxlen+5],pre[maxlen+5],ans[maxlen+5];
inline void change(int ll,int rr)//差分
{
if (ll>rr) return;
ans[ll]++;
ans[rr+1]--;
}
signed main()
{
cin>>n;
for (int i=1;i<=n;i++) cin>>a[i];
cin>>k;
if (n==1)
{
if (k<a[1]) cout<<0<<endl;
else
{
cout<<k-a[1]+1<<endl;
for (int i=a[1];i<=k;i++) cout<<i<<' ';
cout<<endl;
}
return 0;
}
pre[n+1]=k+1;
for (int i=1;i<=n;i++) pre[i]=pre[i-1]+a[i];
while (ii<n)
{
if (pre[ii]>k) break;
ii++;
if (jj<=ii) jj=ii+1;
while (pre[jj]-pre[ii]<pre[ii]&&jj<=n) jj++;
int now=pre[jj]-pre[ii];
if (jj<=n) change(pre[ii],min(now-1,k));
else change(pre[ii],min(pre[ii+1]-1,k));
}
for (int i=1;i<=k;i++)
{
tot+=ans[i];
if (tot>0)
{
pos++;
ans[i]=1;
}
else ans[i]=0;
}
cout<<pos<<endl;
for (int i=1;i<=k;i++)
{
if (ans[i]==1) cout<<i<<' ';
}
cout<<endl;
return 0;
}
P5018
Description
求二叉树中最大的一棵子树,使得它形态对称且各点点权对称。
Solution
直接暴力枚举该子树的根即可,若不满足要求立即跳出,若满足要求用该子树的大小来尝试更新答案。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n,ans=0;
int a[1000005],son[1000005][2],size[1000005];
inline void dfs(int now,int fath)
{
size[now]=1;
for (int i=0;i<2;i++)
{
if (son[now][i]!=-1)
{
dfs(son[now][i],now);
size[now]+=size[son[now][i]];
}
}
}
inline bool check(int l,int r)
{
if (l==-1&&r==-1) return true;
else if (l!=-1&&r!=-1&&a[l]==a[r]&&check(son[l][0],son[r][1])&&check(son[l][1],son[r][0])) return true;
else return false;
}
inline int read()
{
int s=0,w=1;
char ch=getchar();
while (ch<'0'||ch>'9')
{
if (ch=='-') w=-w;
ch=getchar();
}
while (ch>='0'&&ch<='9')
{
s=(s<<1)+(s<<3)+(ch^'0');
ch=getchar();
}
return s*w;
}
signed main()
{
cin>>n;
for (int i=1;i<=n;i++) a[i]=read();
for (int i=1;i<=n;i++) son[i][0]=read(),son[i][1]=read();
dfs(1,0);
for (int i=1;i<=n;i++)
{
if (check(son[i][0],son[i][1])) ans=max(ans,size[i]);
}
cout<<ans<<endl;
return 0;
}
事实上本题还有一种略复杂的方法,即树上哈希,这里不再赘述。
撒花✿✿ヽ(°▽°)ノ✿撒花