【题解】做题记录(2022.9)
可能会断断续续的,是因为可能有的时候忘记了写记录
9.5
今天搞了一天的平衡树,但大部分都是比较基础的操作
[SHOI2009]会场预约
题目分析:
我们考虑重新定义两个区间 \(A,B\) 的关系:
=
:\(A,B\)有交集<
:\(A\) 完全在 \(B\) 的左边>
:\(A\) 完全在 \(B\) 的右边
那么我们就使用 \(set\),在每次插入之前将有交集也就是等于的区间都删掉就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
struct line{
int l,r;
line(){}
line(int _l,int _r){
l = _l,r = _r;
}
};
bool operator < (line a,line b){return a.r < b.l;}
bool operator > (line a,line b){return a.l > b.r;}
bool operator == (line a,line b){return a.r >= b.l && a.l <= b.r;}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
set<line> st;
int q;
scanf("%d",&q);
for(int i=1; i<=q; i++){
char opt;
int l,r;
cin>>opt;
if(opt == 'A'){
cin>>l>>r;
int h = 0;
set<line>::iterator it=st.find(line(l,r));
while(it != st.end()){
h++;st.erase(it);it=st.find(line(l,r));
}
st.insert(line(l,r));
printf("%d\n",h);
}
else{
printf("%d\n",st.size());
}
}
return 0;
}
总结:
一定要灵活地运用 \(set\) 与平衡树之间的关系,对于这个题虽然平衡树可以写,但是显然 \(set\) 更好。
[HNOI2004]宠物收养场
题目分析:
如果当前数有,那么显然是当前数。否则最优的答案一定是当前数的前驱或者后继,然后就转化为了平衡树的基本操作。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e6+5;
const int INF = 1e17+5;
const int MOD = 1000000;
int cnt,rt,x,y,z,val[N],sz[N],key[N],ch[N][2];
int newnode(int x){
++cnt;
val[cnt] = x;sz[cnt] = 1;key[cnt] = rand();
return cnt;
}
void pushup(int now){
sz[now] = sz[ch[now][0]] + sz[ch[now][1]] + 1;
}
int merge(int x,int y){
if(!x || !y) return x + y;
if(key[x] > key[y]){
ch[x][1] = merge(ch[x][1],y);pushup(x);
return x;
}
else{
ch[y][0] = merge(x,ch[y][0]);pushup(y);
return y;
}
}
void split(int now,int v,int &x,int &y){
if(!now){
x = y = 0;return;
}
if(val[now] <= v){
x = now;split(ch[now][1],v,ch[x][1],y);pushup(x);
}
else{
y = now;split(ch[now][0],v,x,ch[y][0]);pushup(y);
}
}
int find_rk(int now,int k){
if(sz[ch[now][0]] >= k)
return find_rk(ch[now][0],k);
else if(sz[ch[now][0]] + 1 == k)
return val[now];
else return
find_rk(ch[now][1],k - sz[ch[now][0]] - 1);
}
void insert(int v){
split(rt,v,x,y);
rt = merge(merge(x,newnode(v)),y);
}
void delet(int v){
split(rt,v,x,y);
split(x,v-1,x,z);
rt = merge(x,y);
}
int find_pre(int v){
split(rt,v-1,x,y);
if(!sz[x]) return -INF;
int ans = find_rk(x,sz[x]);
rt = merge(x,y);
return ans;
}
int find_suf(int v){
split(rt,v,x,y);
int ans = INF;
if(sz[y]) ans = find_rk(y,1);
rt = merge(x,y);
return ans;
}
bool find(int v){
split(rt,v,x,y);
split(x,v-1,x,z);
bool flag = false;
if(sz[z]) flag = true;
rt = merge(merge(x,z),y);
return flag;
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
srand(time(NULL));
int flag = 0;
int q;
scanf("%lld",&q);
int ans = 0;
while(q--){
int opt,h;
scanf("%lld%lld",&opt,&h);
if(sz[rt] == 0){
insert(h);
flag = opt;
continue;
}
if(opt == flag) insert(h);
else{
if(find(h)){
delet(h);
continue;
}
int pre = find_pre(h);
int suf = find_suf(h);
if(abs(pre - h) <= abs(suf - h)){
delet(pre);
ans = (ans + abs(pre-h))%MOD;
}
else{
delet(suf);
ans = (ans + abs(suf - h))%MOD;
}
}
}
printf("%lld\n",ans);
return 0;
}
[HNOI2002]营业额统计
题目分析:
与上个题类似,考虑这个式子其实就可以理解为这两个数在数轴上的距离,所以最优的答案一定是它前面的数中它的前驱或者后继,那么又转化为了平衡树的基本操作。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const long long MAXN = 4e4+5;
const long long INF = 1e18;
struct node{
long long ch[2],fa,cnt,size,val;
node(long long a1=0,long long a2=0,long long a3=0,long long a4=0,long long a5=0,long long a6=0){
ch[0]=a1,ch[1]=a2,fa=a3,cnt=a4,size=a5,val=a6;
}
};
long long n,root,tot,a[MAXN];
node tree[MAXN];
void update(long long x){
tree[x].size = tree[tree[x].ch[1]].size + tree[tree[x].ch[0]].size + tree[x].cnt;
}
void rotate(long long x){
long long y = tree[x].fa;
long long z = tree[y].fa;
long long k = tree[y].ch[1] == x;
tree[z].ch[tree[z].ch[1] == y] = x; tree[x].fa = z;
tree[y].ch[k] = tree[x].ch[k^1]; tree[tree[x].ch[k^1]].fa = y;
tree[x].ch[k^1] = y; tree[y].fa = x;
update(y); update(x);
}
void Splay(long long x,long long goal){
while(tree[x].fa != goal){
long long y = tree[x].fa;
long long z = tree[y].fa;
if(z == goal){
rotate(x);
break;
}
if(z != goal){
if((tree[z].ch[1] == y) == (tree[y].ch[1] == x))
rotate(y);
else
rotate(x);
}
rotate(x);
}
if(goal == 0)
root = x;
}
void find(long long x){ //找到值与 x 在平衡树上的位置
long long now = root;
while(tree[now].val != x && tree[now].ch[x > tree[now].val]){
now = tree[now].ch[x > tree[now].val];
}
// return now;
Splay(now,0);
}
void Insert(long long x){ //插入数 x
long long now = root,fa=0;
while(tree[now].val != x && now){
fa=now;
now = tree[now].ch[x > tree[now].val];
}
if(now){
tree[now].cnt++;
}
else{
now = ++tot;
if(fa){
tree[fa].ch[x > tree[fa].val] = now;
}
tree[now] = node(0,0,fa,1,1,x);
}
Splay(now,0);
}
long long Next(long long x,long long k){ //大于等于 or 小于等于,也就是在前驱和后继的前提下的最接近的数
find(x);
long long now = root;
if((tree[now].val == x) || (tree[now].val < x && !k) || (tree[now].val > x && k))
return tree[now].val;
now = tree[now].ch[k];
while(tree[now].ch[k^1]) now = tree[now].ch[k^1];
return tree[now].val;
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
Insert(INF);
Insert(-INF);
cin>>n;
for(long long i=1; i<=n; i++)
cin>>a[i];
long long ans=0;
for(long long i=1; i<=n; i++){ //保证找到的前驱和后继的编号都小于 i 也就是当前点的编号
long long pre = Next(a[i],0);
long long nxt = Next(a[i],1);
Insert(a[i]);
if(pre == -INF && nxt == INF){
ans += a[i];
}
else if(pre == -INF){
ans += nxt - a[i];
}
else if(nxt == INF){
ans += a[i] - pre;
}
else
ans += min(a[i]-pre,nxt-a[i]);
}
printf("%lld\n",ans);
return 0;
}
[TJOI2007]书架
题目分析:
对于维护区间的 fhq_treap 我们合并的顺序其实就是我们维护的区间上的顺序,所以分裂后按照题目的顺序合并就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
string val[N];
int rt,cnt,x,y,z,sz[N],ch[N][2],key[N];
void pushup(int now){
sz[now] = sz[ch[now][0]] + sz[ch[now][1]] + 1;
}
int merge(int x,int y){
if(!x || !y) return x + y;
if(key[x] > key[y]){
ch[x][1] = merge(ch[x][1],y);
pushup(x);
return x;
}
else{
ch[y][0] = merge(x,ch[y][0]);
pushup(y);
return y;
}
}
void split(int now,int k,int &x,int &y){
if(!now){
x = y = 0;
return;
}
if(sz[ch[now][0]] < k){
x = now;
split(ch[now][1],k - sz[ch[now][0]] - 1,ch[x][1],y);
pushup(x);
}
else{
y = now;
split(ch[now][0],k,x,ch[y][0]);
pushup(y);
}
}
int newnode(string v){
++cnt;
val[cnt] = v;sz[cnt] = 1;key[cnt] = rand();
return cnt;
}
void get_out(int g){
split(rt,g,x,y);
split(x,g-1,x,z);
cout<<val[z]<<endl;
rt = merge(merge(x,z),y);
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n;
scanf("%d",&n);
for(int i=1; i<=n; i++){
string h;
cin>>h;
split(rt,i-1,x,y);
rt = merge(merge(x,newnode(h)),y);
}
int q;
scanf("%d",&q);
for(int i=1; i<=q; i++){
string h;
int g;
cin>>h>>g;
split(rt,g,x,y);
rt = merge(merge(x,newnode(h)),y);
}
scanf("%d",&q);
for(int i=1; i<=q; i++){
int g;
scanf("%d",&g);g++;
get_out(g);
}
return 0;
}
总结:
关键在于对于 \(merge\) 操作的理解,非常关键的一点就是对于维护区间的 fhq_treap,合并顺序即区间上的顺序。
对于这类操作显然 \(Splay\) 就不如 fhq_treap 好做好理解。
[HAOI2008]排名系统
题目分析:
这题也是平衡树的基本操作。
就是注意几点:
- 上传新记录的时候原有记录被覆盖,即需要删除
- 每个人维护一个时间戳,当得分一样时按时间戳排名
- 得分越高排名越小,得分相同时间戳越小排名越小
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 3e5+5;
struct node{
int val,tag;
string name;
node(){}
node(string _name,int _val,int _tag){
name = _name,val = _val,tag = _tag;
}
}val[N];
int cnt,rt,x,y,z,sz[N],key[N],ch[N][2];
map<string,node> mp;
bool operator < (node a,node b){ //注意:如果 val 相等认为越早越靠前
if(a.val == b.val) return a.tag > b.tag;
return a.val < b.val;
}
bool operator == (node a,node b){
return a.val == b.val && a.tag == b.tag;
}
int newnode(node h){
++cnt;
val[cnt] = h;sz[cnt] = 1;key[cnt] = rand();
return cnt;
}
void pushup(int now){
sz[now] = sz[ch[now][0]] + sz[ch[now][1]] + 1;
}
int merge(int x,int y){
if(!x || !y) return x + y;
if(key[x] > key[y]){
ch[x][1] = merge(ch[x][1],y);
pushup(x);
return x;
}
else{
ch[y][0] = merge(x,ch[y][0]);
pushup(y);
return y;
}
}
void split(int now,node k,int &x,int &y){
if(!now){
x = y = 0;
return;
}
if(val[now] < k || val[now] == k){
x = now;
split(ch[now][1],k,ch[x][1],y);
pushup(x);
}
else{
y = now;
split(ch[now][0],k,x,ch[y][0]);
pushup(y);
}
}
string find_rk(int now,int k){ //求第 k 大的
if(sz[ch[now][1]] >= k) return find_rk(ch[now][1],k);
else if(sz[ch[now][1]] + 1 == k) return val[now].name;
else return find_rk(ch[now][0],k - sz[ch[now][1]] - 1);
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
srand(time(NULL));
int n;
scanf("%lld",&n);
for(int i=1; i<=n; i++){
char opt;
cin>>opt;
if(opt == '+'){
string name;
int val;
cin>>name>>val;
if(mp.count(name)){
split(rt,mp[name],x,y);
split(x,node(mp[name].name,mp[name].val,mp[name].tag + 1),x,z);
//这个值就是小于 mp[name] 的最大的值
rt = merge(x,y);
}
node h = node(name,val,i);mp[name] = h;
split(rt,h,x,y);
rt = merge(merge(x,newnode(h)),y);
}
else{
string c;
cin>>c;
if(c[0] >= 'A' && c[0] <= 'Z'){
node h = mp[c];
split(rt,h,x,y);
printf("%lld\n",sz[y]+1);
rt = merge(x,y);
}
else{
int val = 0;
for(int j=0; j<c.size(); j++){
val = val * 10 + c[j] - '0';
}
for(int j=val; j<val + 10 && j <= sz[rt]; j++){
cout<<find_rk(rt,j)<<' ';
}
cout<<endl;
}
}
}
return 0;
}
[JLOI2011]不等式组
题目分析:
这个题显然可以将操作一转换为 \(x > C\) 或 \(x < C\),那么就可以转化为平衡树上的基本操作了。
为了方便可以建两个平衡树,分别维护 \(a>0\) 和 \(a<0\) 的情况。
注意因为精度问题,所以显然不能直接用浮点数,可以考虑上取整或者下取整再通过加一减一来调整。
注意:\(a\) 可能等于 \(0\),此时就需要特判。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
struct Tree{
int cnt = 0;
int rt,x,y,z,val[N],sz[N],key[N],ch[N][2];
void pushup(int now){
sz[now] = sz[ch[now][0]] + sz[ch[now][1]] + 1;
}
int newnode(int v){
++cnt;
val[cnt] = v;sz[cnt] = 1;key[cnt] = rand();
return cnt;
}
int merge(int x,int y){
if(!x || !y) return x + y;
if(key[x] > key[y]){
ch[x][1] = merge(ch[x][1],y);
pushup(x);
return x;
}
else{
ch[y][0] = merge(x,ch[y][0]);
pushup(y);
return y;
}
}
void split(int now,int k,int &x,int &y){
if(!now){
x = y = 0;
return;
}
if(val[now] <= k){
x = now;
split(ch[now][1],k,ch[x][1],y);
pushup(x);
}
else{
y = now;
split(ch[now][0],k,x,ch[y][0]);
pushup(y);
}
}
void insert(int v){
split(rt,v,x,y);
rt = merge(merge(x,newnode(v)),y);
}
void delet(int v){
split(rt,v,x,y);
split(x,v-1,x,z);
z = merge(ch[z][0],ch[z][1]);
rt = merge(merge(x,z),y);
}
int query_mx(int v){
split(rt,v-1,x,y);
int ans = sz[y];
rt = merge(x,y);
return ans;
}
int query_mn(int v){
split(rt,v,x,y);
int ans = sz[x];
rt = merge(x,y);
return ans;
}
}t1,t2;
int opt1[N],tmp1[N],X[N],Y[N],Z[N];
bool flag[N];
char opt[N];
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n;
scanf("%d",&n);
int ans = 0;
int tot = 0;
for(int i=1; i<=n; i++){
scanf("%s",opt+1);
if(opt[1] == 'A'){
++tot;
flag[tot] = true;
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
X[tot] = a;Y[tot] = b;Z[tot] = c;
if(a == 0) ans += b > c;
else if(a < 0){
tmp1[tot] = ceil((c-b) / (a * 1.0)) - 1;
opt1[tot] = 1;
t1.insert(tmp1[tot]);
}
else{
tmp1[tot] = floor((c-b) / (a * 1.0)) + 1;
opt1[tot] = 0;
t2.insert(tmp1[tot]);
}
}
else if(opt[1] == 'D'){
int a;
scanf("%d",&a);
if(flag[a]){
if(X[a] == 0 && Y[a] > Z[a]) ans--;
if(opt1[a] == 1) t1.delet(tmp1[a]);
if(opt1[a] == 0) t2.delet(tmp1[a]);
flag[a] = false;
}
}
else{
int k;
scanf("%d",&k);
printf("%d\n",ans + t1.query_mx(k) + t2.query_mn(k));
}
}
return 0;
}
*[TJOI2013]最长上升子序列
题目分析:
对于最长上升子序列,显然需要考虑 \(dp\) 做法。
即设 \(dp[i]\) 表示以 \(i\) 为结尾的最长上升子序列的长度。
转移即:
我们考虑新加入一个数会造成什么影响,注意:我们加入数的顺序是 \(1\) 到 \(n\)
假设在 \(x\) 位置加入,因为它比所有数都大所以不会对后面的数的 \(dp\) 值产生影响。
因为我们的 \(dp[i]\) 代表的是以 \(i\) 结尾,那么它也不会对前面的 \(dp\) 值产生影响。
所以唯一会变化的也就是当前数也就是新出现的 \(dp\) 值,显然 \(dp[x] = \max_{j=1}^{x-1} dp[j] + 1\),因为它比所有的数都大。
那么这也就是一个区间求最大值和单点插入的操作,使用平衡树就可以解决。
注意插入的位置对应的 \(merge\) 的顺序。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
int rt,x,y,z,cnt,dp[N],val[N],ch[N][2],sz[N],key[N],pos[N];
void pushup(int now){
val[now] = max(dp[pos[now]],max(val[ch[now][0]],val[ch[now][1]]));
sz[now] = sz[ch[now][0]] + sz[ch[now][1]] + 1;
}
int merge(int x,int y){
if(!x || !y) return x | y;
if(key[x] < key[y]){
ch[x][1] = merge(ch[x][1],y);
pushup(x);
return x;
}
else{
ch[y][0] = merge(x,ch[y][0]);
pushup(y);
return y;
}
}
void split(int now,int k,int &x,int &y){ //分裂排名也就相当于在维护区间
if(!now){
x = y = 0;
return;
}
if(sz[ch[now][0]] < k){
x = now;
split(ch[now][1],k - sz[ch[now][0]] - 1,ch[x][1],y);
}
else{
y = now;
split(ch[now][0],k,x,ch[y][0]);
}
pushup(now);
}
int newnode(int v,int i){
++cnt;
val[cnt] = v;sz[cnt] = 1;key[cnt] = rand();pos[cnt] = i;
return cnt;
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n;
scanf("%d",&n);
for(int i=1; i<=n; i++){
int pos;
scanf("%d",&pos);
split(rt,pos,x,y);
dp[i] = val[x] + 1;
// printf("%d\n",dp[i]);
rt = merge(merge(x,newnode(dp[i],i)),y);
printf("%d\n",val[rt]);
}
return 0;
}
总结:
从最基础的套路出发,一点点的发现问题的性质,然后最后转化为一个可以做的问题
[TJOI2010]中位数
题目分析:
显然这个题可以使用对顶堆维护,但是我们也可以考虑使用平衡树。
考虑中位数也就是可以理解为一个区间第 \(k\) 大的问题,那么也就是平衡树的基础操作了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
int cnt,x,y,z,rt,key[N],ch[N][2],sz[N],val[N];
char opt[100];
int newnode(int v){
++cnt;
key[cnt] = rand();sz[cnt] = 1;val[cnt] = v;
return cnt;
}
void pushup(int now){
sz[now] = sz[ch[now][0]] + sz[ch[now][1]] + 1;
}
int merge(int x,int y){
if(!x || !y) return x + y;
if(key[x] > key[y]){
ch[x][1] = merge(ch[x][1],y);
pushup(x);
return x;
}
else{
ch[y][0] = merge(x,ch[y][0]);
pushup(y);
return y;
}
}
void split(int now,int k,int &x,int &y){
if(!now){x = y = 0;return;}
if(val[now] <= k){
x = now;
split(ch[now][1],k,ch[x][1],y);
pushup(x);
}
else{
y = now;
split(ch[now][0],k,x,ch[y][0]);
pushup(y);
}
}
void insert(int v){
split(rt,v,x,y);
rt = merge(merge(x,newnode(v)),y);
}
int query(int now,int k){
if(sz[ch[now][0]] >= k) return query(ch[now][0],k);
else if(sz[ch[now][0]] + 1 == k) return val[now];
else return query(ch[now][1],k - sz[ch[now][0]] - 1);
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n;
scanf("%d",&n);
for(int i=1; i<=n; i++){
int a;
scanf("%d",&a);
insert(a);
}
int q;
scanf("%d",&q);
while(q--){
int x;
scanf("%s",opt + 1);
if(opt[1] == 'a'){
n++;scanf("%d",&x);
insert(x);
}
else if(opt[1] == 'm'){
if(n % 2 == 0) printf("%d\n",query(rt,n/2)); //求第 k 小
else printf("%d\n",query(rt,(n/2) + 1));
}
}
return 0;
}
[TJOI2014]电源插排
题目分析:
我这个题比较麻烦,使用了 \(set\) + \(map\) + fhq_treap
先考虑如果我们可以知道每一个数的插入位置,那么我们求 \([l,r]\) 中插头的个数,也就是有多少个数满足大于等于 \(l\) 且小于等于 \(r\),平衡树的基本操作。
我们就可以考虑使用两个 \(set\) 维护每一个数插入的位置以及每一个区间的左右端点。
每一次插入一个数也就是找到最长的且最靠右的区间,这个可以通过重新定义小于来实现,然后将这个区间分裂为两个区间,然后再维护。
每一次删除一个数也就是找到这个数的位置,然后删除原本以这个数为左右端点的两个区间,然后再将这个两个合并后放回去。
注意因为我们同学的编号很大,所以需要使用 \(map\) 离散化。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6+5;
struct node{
int l,r;
node(){}
node(int _l,int _r){
l = _l,r = _r;
}
};
bool operator < (node a,node b){
if(a.r - a.l == b.r - b.l) return a.l > b.l;
return a.r - a.l > b.r - b.l;
}
multiset<int> st1;
multiset<node> len;
int tag[N],tot,x,y,z,rt,cnt,val[N],key[N],sz[N],ch[N][2];
map<int,int> mp;
void pushup(int now){
sz[now] = sz[ch[now][0]] + sz[ch[now][1]] + 1;
}
int newnode(int v){
++cnt;
sz[cnt] = 1;val[cnt] = v;key[cnt] = rand();
return cnt;
}
int merge(int x,int y){
if(!x || !y) return x + y;
if(key[x] > key[y]){
ch[x][1] = merge(ch[x][1],y);
pushup(x);
return x;
}
else{
ch[y][0] = merge(x,ch[y][0]);
pushup(y);
return y;
}
}
void split(int now,int k,int &x,int &y){
if(!now){
x = y = 0;
return;
}
if(val[now] <= k){
x = now;
split(ch[now][1],k,ch[x][1],y);
pushup(x);
}
else{
y = now;
split(ch[now][0],k,x,ch[y][0]);
pushup(y);
}
}
void insert(int v){
split(rt,v,x,y);
rt = merge(merge(x,newnode(v)),y);
}
void delet(int v){
split(rt,v,x,y);
split(x,v-1,x,z);
z = merge(ch[z][0],ch[z][1]);
rt = merge(merge(x,z),y);
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n,q;
scanf("%lld%lld",&n,&q);
len.insert(node(0,n+1));
st1.insert(0);
st1.insert(n+1);
while(q--){
int k;
scanf("%lld",&k);
if(k != 0){ //用 set 维护区间找到插的位置,用 fhq_treap 维护具体地插以及查询。
if(!mp.count(k)){
mp[k] = ++tot;
}
k = mp[k];
if(tag[k]){
multiset<int>::iterator it;
it = st1.find(tag[k]);
--it;
int pre = *it;it++;it++;
int suf = *it;
len.erase(len.find(node(pre,tag[k])));
// printf("Erase %lld %lld\n",pre,tag[k]);
len.erase(len.find(node(tag[k],suf)));
// printf("Erase %lld %lld\n",tag[k],suf);
len.insert(node(pre,suf));
// printf("Insert %lld %lld\n",pre,suf);
st1.erase(st1.find(tag[k]));
delet(tag[k]);
tag[k] = 0;
}
else{
multiset<node>::iterator it;
it = len.begin();
node a = *it;
tag[k] = (a.r + a.l)/2;
if((a.r - a.l + 1) % 2 == 0) tag[k]++;
len.erase(len.find(node(a.l,a.r)));
// printf("Erase %lld %lld\n",a.l,a.r);
len.insert(node(a.l,tag[k]));
// printf("Insert %lld %lld\n",a.l,tag[k]);
len.insert(node(tag[k],a.r));
// printf("Insert %lld %lld\n",tag[k],a.r);
st1.insert(tag[k]);
insert(tag[k]);
}
}
else{
int l,r;
scanf("%lld%lld",&l,&r);
split(rt,l-1,x,y);
split(y,r,y,z);
printf("%lld\n",sz[y]);
rt = merge(x,merge(y,z));
}
}
return 0;
}
总结:
发挥 \(set\) 与平衡树各自的容易实现的地方,然后结合起来使用。
[SHOI2013]发牌
题目分析:
我们的销牌其实就可以理解为将前缀的一个区间转移到它的后缀上去,根据 fhq_treap 的 \(merge\) 的性质,直接将前缀区间拿出来,然后反着合并这个前缀和剩下的区间就好了。
对于发牌其实就是找到区间上最左边的点,也就是一直跳左儿子就好了,或者也可以理解为排名为 \(1\) 的数,写一个找排名的函数也行。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+5;
int cnt,rt,x,y,z,sz[N],val[N],key[N],ch[N][2];
inline int read(){
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
int newnode(int v){
++cnt;
val[cnt] = v;key[cnt] = rand();sz[cnt] = 1;
return cnt;
}
void pushup(int now){
sz[now] = sz[ch[now][0]] + sz[ch[now][1]] + 1;
}
int merge(int x,int y){
if(!x || !y) return x + y;
if(key[x] < key[y]){
ch[x][1] = merge(ch[x][1],y);
pushup(x);
return x;
}
else{
ch[y][0] = merge(x,ch[y][0]);
pushup(y);
return y;
}
}
void split(int now,int k,int &x,int &y){
if(!now){
x = y = 0;
return;
}
if(sz[ch[now][0]] < k){
x = now;
split(ch[now][1],k - sz[ch[now][0]] - 1,ch[x][1],y);
pushup(x);
}
else{
y = now;
split(ch[now][0],k,x,ch[y][0]);
pushup(y);
}
}
int find_rk(int now,int k){
if(sz[ch[now][0]] >= k) return find_rk(ch[now][0],k);
else if(sz[ch[now][0]] + 1 == k) return val[now];
else return find_rk(ch[now][1],k - sz[ch[now][0]] - 1);
}
int build(int l,int r){
if(l > r) return 0;
int mid = (l + r)>>1;
int now = newnode(mid);
if(l == r){return now;}
ch[now][0] = build(l,mid-1);
ch[now][1] = build(mid+1,r);
return now;
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n;
n=read();
for(int i=1; i<=n; i++){rt = merge(rt,newnode(i));}
int len = n;
for(int i=1; i<=n; i++){
int r;
r=read();
r %= len;
split(rt,r,x,y);
rt = merge(y,x);
int now = rt;
while(ch[now][0]) now = ch[now][0];
printf("%d\n",val[now]);
split(rt,1,x,y);
rt = y;
len--;
}
return 0;
}
9.7
[算法竞赛进阶指南]牧师约翰最忙碌的一天
题目分析:
看上去就很 2-SAT
考虑我们将每场婚礼拆成两个状态:在 \(S\) 的时候举行、在 \(T\) 的时候举行。
我们就考虑直接枚举任意的两场婚礼,判断这两个时间段的关系,这样就可以建出一个图。
对图做一遍 2-SAT 就是答案。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e4+5;
const int M = 4e6+5;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[M];
int cnt,tot,top,col,head[N],dfn[N],low[N],st[N],color[N],S[N],T[N],D[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
bool check(int l1,int r1,int l2,int r2){
return r1 > l2 && l1 < r2;
}
void tarjan(int now){
dfn[now] = low[now] = ++tot;
st[++top] = now;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(!dfn[to]){
tarjan(to);
low[now] = min(low[now],low[to]);
}
else if(!color[to]){
low[now] = min(low[now],dfn[to]);
}
}
if(low[now] == dfn[now]){
++col;
color[now] = col;
while(st[top] != now){
color[st[top]] = col;
top--;
}
top--;
}
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n;
scanf("%d",&n);
for(int i=1; i<=n; i++){
int S_h,S_m,T_h,T_m,d;
scanf("%d:%d %d:%d %d",&S_h,&S_m,&T_h,&T_m,&d);
S[i] = S_h * 60 + S_m;
T[i] = T_h * 60 + T_m;
D[i] = d;
}
for(int i=1; i<=n; i++){
for(int j=1; j<=n; j++){
if(i == j) continue;
if(check(S[i],S[i] + D[i],S[j],S[j] + D[j])) //有交
add_edge(2 * i,2 * j + 1);
if(check(S[i],S[i] + D[i],T[j] - D[j],T[j]))
add_edge(2 * i,2 * j);
if(check(T[i] - D[i],T[i],S[j],S[j] + D[j]))
add_edge(2 * i + 1,2 * j + 1);
if(check(T[i] - D[i],T[i],T[j] - D[j],T[j]))
add_edge(2 * i + 1,2 * j);
}
}
for(int i=1; i<=n*2+1; i++){
if(!dfn[i])
tarjan(i);
}
// for(int i=1; i<=n; i++){
// printf("%d:%d %d\n",i,color[2 * i],color[2 * i + 1]);
// }
for(int i=1; i<=n; i++){
if(color[2 * i] == color[2 * i + 1]){
printf("NO\n");
return 0;
}
}
printf("YES\n");
for(int i=1; i<=n; i++){
if(color[2 * i] < color[2 * i + 1]){
int l = S[i],r = S[i] + D[i];
printf("%02d:%02d %02d:%02d\n",l/60,l%60,r/60,r%60);
}
else{
int l = T[i] - D[i],r = T[i];
printf("%02d:%02d %02d:%02d\n",l/60,l%60,r/60,r%60);
}
}
return 0;
}
[NOI2017]游戏
题目分析:
看着这个特殊要求就很有 2-SAT 的味道。
我们考虑如果没有 \(x\) 型地图的话显然每一种地图都只可以跑两种车,可以分别定义为取正和取反,这样就很显然可以转化为 2-SAT 问题。
那么我们也可以发现一个性质:\(x\) 的个数很小,而因为在我们最后的方案里 \(x\) 地图的车一定是一个确定的值,所以就可以考虑直接枚举 \(x\) 可以成为哪种地图,然后以这种地图处理就好了。显然我们不用三种地图都枚举,只需要 \(a,b\) 两种地图就可以涵盖所有 \(x\) 可能的值,即 \(A,B,C\)。
注意:在建图上需要加一点特判,以及注意各个东西的标号
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+5;
const int M = 3e6+5;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[M];
int cnt,tot,top,col,m,n,head[N],dfn[N],low[N],color[N],st[N],a[N],c[N];
char s[N],tmp[N],b[N],d[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
int get_num(int i,char x,char y){
if(x == 'a'){
if(y == 'b') return i * 2;
else return i * 2 + 1;
}
if(x == 'b'){
if(y == 'a') return i * 2;
return i * 2 + 1;
}
if(x == 'c'){
if(y == 'a') return i * 2;
return i * 2 + 1;
}
}
void tarjan(int now){
dfn[now] = low[now] = ++tot;
st[++top] = now;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(!dfn[to]){
tarjan(to);
low[now] = min(low[now],low[to]);
}
else if(!color[to]){
low[now] = min(low[now],dfn[to]);
}
}
if(low[now] == dfn[now]){
++col;
color[now] = col;
while(st[top] != now){
color[st[top]] = col;
top--;
}
top--;
}
}
bool check(int S){
// printf("状态 %d:\n",S);
memset(color,0,sizeof(color));memset(st,0,sizeof(st));memset(head,0,sizeof(head));memset(low,0,sizeof(low));memset(dfn,0,sizeof(dfn));
tot = cnt = col = top = 0;
int h = strlen(tmp+1);
int res = 0;
for(int i=1; i<=h; i++){
s[i] = tmp[i];
if(s[i] == 'x'){
++res;
if(S & (1<<(res-1))) //不选 A 或者 不选 B已经可以涵盖所有的情况,因为只会选择一种
s[i] = 'a';
else
s[i] = 'b';
}
}
/*
a:正 B;反 C
b:正 A;反 C
c:正 A;反 B
*/
for(int i=1; i<=m; i++){
// cout<<a[i]<<' '<<b[i]<<' '<<c[i]<<' '<<d[i]<<':'<<endl;
if(s[a[i]] == b[i]) continue; //当题设不成立时,也就不用管了
if(s[c[i]] == d[i]){ //注意当结论不成立时,即假设不能成立
int u = get_num(a[i],s[a[i]],b[i]);
add_edge(u,u^1);
// printf("%d %d\n",u,u^1);
}
else{
int u = get_num(a[i],s[a[i]],b[i]); //正常连边
int v = get_num(c[i],s[c[i]],d[i]);
add_edge(u,v); //原命题等价于逆否命题
add_edge(v^1,u^1);
// printf("%d %d\n",u,v);
// printf("%d %d\n",v^1,u^1);
}
// printf("\n");
}
// printf("\n");
for(int i=1; i<=h * 2 + 1; i++){
if(!dfn[i])
tarjan(i);
}
for(int i=1; i<=h; i++){
if(color[i * 2] == color[i * 2 + 1])
return false;
}
for(int i=1; i<=h; i++){
if(color[i * 2] < color[i * 2 + 1]){
if(s[i] == 'a') cout<<'B';
if(s[i] == 'b') cout<<'A';
if(s[i] == 'c') cout<<'A';
}
else{
if(s[i] == 'a') cout<<'C';
if(s[i] == 'b') cout<<'C';
if(s[i] == 'c') cout<<'B';
}
}
return true;
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int p;
scanf("%d%d",&n,&p);
scanf("%s",tmp+1);
scanf("%d",&m);
for(int i=1; i<=m; i++){
cin>>a[i]>>b[i]>>c[i]>>d[i];
b[i] = b[i] - 'A' + 'a';
d[i] = d[i] - 'A' + 'a';
}
for(int i=0; i<(1<<p); i++){
if(check(i)){
return 0;
}
}
// if(p == 0){
// if(check(0)){
// return 0;
// }
// }
printf("-1\n");
return 0;
}
总结:
要善于简化问题。
而且对于某一些非常小的量可以考虑直接枚举。
[ZJOI2012]网络
题目分析:
我们可以观察到一个性质,颜色的数量很少,所以可以考虑每种颜色分开维护。
我们发现如果分开维护之后操作 \(1\) 和操作 \(3\) 就是平衡树的基本操作,而对于操作 \(2\) 就相当于断边 \(+\) 连边,所以上一个 \(LCT\) 维护就好了。
这题比较麻烦的就是操作 \(2\) 的几种情况的判断:
- 如果更改后的颜色与原颜色相同就直接不改
- 不存在连接的边:把所有的可能枚举一下,如果什么情况都不满足就是没有连边。
- 不存在相同的超过两条:直接记录连出去多少条边就好了
- 不存在环:连的时候如果两个点已经联通,那么就说明存在环
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e4+5;
struct LCT{
int top,st[N],ch[N][2],mx[N],tag[N],fa[N],val[N],deg[N];
int getson(int x){return ch[fa[x]][1] == x;}
bool isroot(int x){return ch[fa[x]][getson(x)]!=x;}
void pushup(int now){
mx[now] = max(val[now],max(mx[ch[now][0]],mx[ch[now][1]]));
}
void pushdown(int now){
if(!tag[now]) return;
swap(ch[now][0],ch[now][1]);
tag[ch[now][0]]^=1;tag[ch[now][1]]^=1;
tag[now]^=1;
}
void rotate(int x){
int y = fa[x],z = fa[y],k = getson(x);
if(!isroot(y)) ch[z][getson(y)] = x;fa[x] = z;
ch[y][k] = ch[x][k^1];if(ch[y][k]) fa[ch[y][k]] = y;
ch[x][k^1] = y;fa[y] = x;
pushup(y);pushup(x);
}
void splay(int x){
top=1;st[1] = x;
for(int i=x;!isroot(i);i=fa[i]) st[++top] = fa[i];
for(int i=top; i; i--) pushdown(st[i]);
for(;!isroot(x);rotate(x)){
if(!isroot(fa[x])) rotate(getson(x) == getson(fa[x]) ? fa[x] : x);
}
}
void access(int x){
for(int pre=0;x;pre=x,x=fa[x]){
splay(x);ch[x][1] = pre;
pushup(x);
}
}
void makeroot(int x){
access(x);splay(x);
tag[x]^=1;
}
int findroot(int x){
access(x);splay(x);
while(ch[x][0]) x = ch[x][0];
return x;
}
void link(int u,int v){
makeroot(u);
fa[u] = v;
deg[u]++;deg[v]++;
}
void cut(int u,int v){
makeroot(u);access(v);splay(v);
if(fa[u] == v && !ch[u][1]){
ch[v][0] = fa[u] = 0;
deg[u]--;deg[v]--;
}
pushup(v);
}
void split(int x,int y){
makeroot(x);
access(y);
splay(y);
}
void change(int x,int v){
splay(x);
val[x] = v;
pushup(x);
}
}T[11];
int val[N];
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n,m,c,k;
scanf("%d%d%d%d",&n,&m,&c,&k);
for(int i=1; i<=n; i++){
scanf("%d",&val[i]);
for(int j=0; j<c; j++){
T[j].val[i] = val[i];
}
}
for(int i=1; i<=m; i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
T[w].link(u,v);
}
for(int i=1; i<=k; i++){
int opt,x,y,z;
scanf("%d",&opt);
if(opt == 0){
scanf("%d%d",&x,&y);
for(int j=0; j<c; j++){
T[j].change(x,y);
}
}
else if(opt == 1){
scanf("%d%d%d",&x,&y,&z);
bool flag = false;
for(int j=0; j<c; j++){
if(T[j].findroot(x) == T[j].findroot(y)){
T[j].split(x,y);
if(T[j].ch[y][0] != x || T[j].ch[x][1]) continue;
if(j == z){
printf("Success.\n");
flag = true;
break;
}
if((T[z].deg[x] == 2) || (T[z].deg[y] == 2)){
printf("Error 1.\n");
flag = true;
break;
}
else if(T[z].findroot(x) == T[z].findroot(y)){
printf("Error 2.\n");
flag = true;
break;
}
else{
T[j].cut(x,y);T[z].link(x,y);
printf("Success.\n");
flag = true;
break;
}
}
}
if(!flag)
printf("No such edge.\n");
}
else if(opt == 2){
scanf("%d%d%d",&x,&y,&z);
if(T[x].findroot(y) != T[x].findroot(z)){
printf("-1\n");
}
else{
T[x].split(y,z);
printf("%d\n",T[x].mx[z]);
}
}
}
return 0;
}
9.8
[HNOI2012]永无乡
题目分析:
显然对于操作 \(2\) 可以使用权值线段树或者平衡树解决。
而对于操作 \(1\) 就相当于一个数据结构的合并,为了避免合并已经合并在一起的东西,我们可以使用并查集判一下连通性。
对于线段树可以使用线段树合并,对于平衡树可以考虑启发式合并。
我写的是平衡树的启发式合并。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+5;
int cnt,x,y,z,val[N],rt[N],tot[N],fa[N],sz[N],ch[N][2],key[N],p[N],tmp[N];
int newnode(int v){
++cnt;
val[cnt] = v;key[cnt] = rand();sz[cnt] = 1;
return cnt;
}
void pushup(int now){
sz[now] = sz[ch[now][0]] + sz[ch[now][1]] + 1;
}
int merge(int x,int y){
if(!x || !y) return x + y;
if(key[x] > key[y]){
ch[x][1] = merge(ch[x][1],y);
pushup(x);
return x;
}
else{
ch[y][0] = merge(x,ch[y][0]);
pushup(y);
return y;
}
}
void split(int now,int k,int &x,int &y){
if(!now){
x = y = 0;
return;
}
if(val[now] <= k){
x = now;
split(ch[now][1],k,ch[x][1],y);
pushup(x);
}
else{
y = now;
split(ch[now][0],k,x,ch[y][0]);
pushup(y);
}
}
void insert(int &root,int v){
split(root,v,x,y);
root = merge(merge(x,newnode(v)),y);
}
void delet(int &root,int v){
split(root,v,x,y);
split(x,v-1,x,z);
z = merge(ch[z][0],ch[z][1]);
root = merge(merge(x,z),y);
}
int find(int x){
if(fa[x] == x) return x;
return fa[x] = find(fa[x]);
}
void tree_merge(int &x,int &y){
while(sz[x]){
insert(y,val[x]);
delet(x,val[x]);
}
}
void hebing(int u,int v){
u = find(u),v = find(v);
if(u != v){
if(tot[u] < tot[v]) swap(u,v);
tree_merge(rt[v],rt[u]);
fa[v] = u;tot[u] += tot[v];
}
}
int find_val(int now,int k){
if(sz[now] < k) return -1;
if(sz[ch[now][0]] >= k) return find_val(ch[now][0],k);
else if(sz[ch[now][0]] + 1 == k) return val[now];
else return find_val(ch[now][1],k - sz[ch[now][0]] - 1);
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n,m;
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++) scanf("%d",&p[i]),tmp[p[i]] = i;
for(int i=1; i<=n; i++){
fa[i] = i;tot[i] = 1;
rt[i] = newnode(p[i]);
}
for(int i=1; i<=m; i++){
int u,v;
scanf("%d%d",&u,&v);
hebing(u,v);
}
int q;
scanf("%d",&q);
while(q--){
char opt[10];
int a,b;
scanf("%s",opt+1);
scanf("%d%d",&a,&b);
// cin>>opt>>a>>b;
if(opt[1] == 'B'){
hebing(a,b);
}
else if(opt[1] == 'Q'){
a = find(a);
int ans = find_val(rt[a],b);
if(ans != -1) printf("%d\n",tmp[ans]);
else printf("%d\n",ans);
}
}
return 0;
}
[NOI2003] 文本编辑器
题目分析:
仿佛正解是块状数组,但是我写的是平衡树。
对于插入和删除操作显然一个平衡树就很好去维护。
需要一点:对于插入长度为 \(n\) 的字符串,一般的做法就是直接对这个字符串建一棵平衡树,但是如果直接以 \(O(n \log n)\) 建树过不去,所以就可以类似笛卡尔树 \(O(n)\) 建树。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 5e6+5;
int tot,rt,st[N],key[N],sz[N],ch[N][2];
char c[N],val[N];
int newnode(char v){
++tot;
val[tot] = v;key[tot] = rand();sz[tot] = 1;
return tot;
}
void pushup(int now){
sz[now] = sz[ch[now][0]] + sz[ch[now][1]] + 1;
}
int merge(int x,int y){
if(!x || !y) return x + y;
if(key[x] > key[y]){
ch[x][1] = merge(ch[x][1],y);
pushup(x);
return x;
}
else{
ch[y][0] = merge(x,ch[y][0]);
pushup(y);
return y;
}
}
void split(int now,int k,int &x,int &y){
if(!now){
x = y = 0;
return;
}
if(sz[ch[now][0]] < k){
x = now;
split(ch[now][1],k - sz[ch[now][0]] - 1,ch[x][1],y);
pushup(x);
}
else{
y = now;
split(ch[now][0],k,x,ch[y][0]);
pushup(y);
}
}
void get_string(char *s,int len){
int now = 0;
while(now < len){
char c = getchar();
if(c >= 32 && c <= 126) s[++now] = c;
}
}
int build(char *s,int len){
int top = 0;
for(int i=1; i<=len; i++){ //第一维可以认为排好序了,就看第二维了
newnode(s[i]);int lst = 0;
while(top > 0 && key[st[top]] > key[tot]) pushup(st[top]),lst = st[top--];
//找到它的根
if(top) ch[st[top]][1] = tot;
ch[tot][0] = lst; //前面的都是它的子树
st[++top] = tot;
}
while(top) pushup(st[top--]);
return st[1];
}
void print_tree(int now){
if(!now) return;
if(ch[now][0]) print_tree(ch[now][0]);
// cout<<val[now];
printf("%c",val[now]);
if(ch[now][1]) print_tree(ch[now][1]);
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int t;
scanf("%d",&t);
int now = 0;
int x,y,z;
while(t--){
string opt;
cin>>opt;
if(opt == "Move"){
scanf("%d",&now);
}
else if(opt == "Insert"){
int len;
scanf("%d",&len);
get_string(c,len);
z = build(c,len);
split(rt,now,x,y);
rt = merge(merge(x,z),y);
}
else if(opt == "Delete"){
int len;
scanf("%d",&len);
split(rt,now,x,y);
split(y,len,y,z);
rt = merge(x,z);
}
else if(opt == "Get"){
int len;
scanf("%d",&len);
split(rt,now,x,y);
split(y,len,y,z);
print_tree(y);
printf("\n");
rt = merge(x,merge(y,z));
}
else if(opt == "Prev"){
now--;
if(now < 0) now = 0;
}
else if(opt == "Next"){
now++;
if(now > sz[rt]) now = sz[rt];
}
}
return 0;
}
9.11
CF1253F Cheap Robot
题目分析:
我们显然需要预处理 \(dis[i]\) 表示 \(i\) 到充电站的最短距离。
这个预处理可以通过建立一个超级源点向所有充电站建边然后跑最短路做到。
我们可以发现以下的式子,假设我们现在在 \(u\) 点,走过了长度为 \(d\) 的边到达了 \(v\) 点,电池容量为 \(x\),当前电量为 \(y\)。
将上面的式子带入下面就可以得到:\(x \ge dis[i] + dis[j] + d\)
所以我们将边 \((u,v)\) 的权值更换为 \(dis[i] + dis[j] + d\),这样就只需要求一个 \(x\) 到 \(y\) 的路径上的最大值最小就好了,也就是建最小生成树然后跑倍增
代码:
点击查看代码
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const long long MAXN = 4e5+5;
const long long MAXM = 6e5+5;
struct edge{
long long from, to, w, nxt;
}e[MAXM << 2], V[MAXM], V2[MAXM << 1];
long long head[MAXN], num_edge = 0;
long long Head[MAXN], Num_edge = 0;
struct node{
long long bh, val;
bool operator < (const node &b) const { return val > b.val; }
};
long long n, m, k, Q, cnt;
long long dis[MAXN], fa[MAXN], f[MAXN][22], deep[MAXN], maxm[MAXN][22];
bool vis[MAXN];
priority_queue<node> q;
long long read(){
long long s = 0, f = 0;
char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar();
return f ? -s : s;
}
bool cmp(edge x, edge y){ return x.w < y.w; }
void add_edge(long long from, long long to, long long w){ e[++num_edge] = (edge){from, to, w, head[from]}, head[from] = num_edge; }
void Add_edge(long long from, long long to, long long w){ V2[++Num_edge] = (edge){from, to, w, Head[from]}, Head[from] = Num_edge; }
long long find(long long x){
if(fa[x] == x)
return x;
return fa[x] = find(fa[x]);
}
void dij(){//跑一边dij求出所有点到最近的充电站的距离
memset(dis, 0x3f, sizeof dis);
dis[0] = 0;
q.push((node){0, 0});
while(!q.empty()){
node u = q.top(); q.pop();
if(vis[u.bh]) continue;
vis[u.bh] = true;
for(long long i = head[u.bh]; i; i = e[i].nxt){
long long v = e[i].to;
if(dis[v] > dis[u.bh] + e[i].w){
dis[v] = dis[u.bh] + e[i].w;
if(!vis[v]) q.push((node){v, dis[v]});
}
}
}
}
void kruskal(){//建出更新权值后的图的最小生成树
for(long long i = 1; i <= n; ++i) fa[i] = i;//重置父亲
for(long long i = 1; i <= m; ++i){
long long from = find(V[i].from), to = find(V[i].to);
if(from != to){
fa[to] = from;
Add_edge(V[i].from, V[i].to, V[i].w), Add_edge(V[i].to, V[i].from, V[i].w);//建出最小生成树来
cnt++;
if(cnt == n - 1) return ;//如果建了n - 1条边,就结束
}
}
}
void dfs(long long u, long long fa){//dfs预处理lca ,注意这里跑的是新图
f[u][0] = fa;
for(long long i = Head[u]; i; i = V2[i].nxt){
long long v = V2[i].to;
if(v == fa) continue;
deep[v] = deep[u] + 1;
maxm[v][0] = V2[i].w;
dfs(v, u);
}
}
void init(){//倍增法预处理lca,顺便维护一个最大值
for(long long i = 1; i <= 20; ++i){
for(long long j = 1; j <= n; ++j){
f[j][i] = f[f[j][i - 1]][i - 1];
maxm[j][i] = max(maxm[j][i - 1], maxm[f[j][i - 1]][i - 1]);
}
}
}
long long get_max(long long x, long long y){//可以直接在求两点lca的过程中求出两点简单路径的最大值
long long ans = 0;
if(deep[x] < deep[y]) swap(x, y);
for(long long i = 20; i >= 0; --i){
if(deep[f[x][i]] < deep[y]) continue;
ans = max(ans, maxm[x][i]);
x = f[x][i];
}
if(x == y) return ans;
for(long long i = 20; i >= 0; --i){
if(f[x][i] == f[y][i]) continue;
ans = max(ans, max(maxm[x][i], maxm[y][i]));
x = f[x][i];
y = f[y][i];
}
ans = max(ans, max(maxm[x][0], maxm[y][0]));
return ans;
}
int main(void)
{
n = read(), m = read(), k = read(), Q = read();
for(long long i = 1, u, v, w; i <= m; ++i){
u = read(), v = read(), w = read();
add_edge(u, v, w), add_edge(v, u, w);
V[i].from = u, V[i].to = v, V[i].w = w;
}
for(long long i = 1; i <= k; ++i){
add_edge(0, i, 0), add_edge(i, 0, 0);
}
dij();
for(long long i = 1; i <= m; ++i){//更新边的权值
V[i].w += dis[V[i].from] + dis[V[i].to];
}
sort(V + 1, V + m + 1, cmp);//按边权大小由小到大排序
kruskal();
deep[1] = 1;
dfs(1, -1);
init();
for(long long i = 1, u, v; i <= Q; ++i){
u = read(), v = read();
cout<<get_max(u, v)<<endl;//直接输出所求结果即可
}
return 0;
}
HDU4786 Fibonacci Tree
题目分析:
我们找到白边最多和最小的生成树,如果存在斐波那契数在这两个生成树的白边的个数之间即有解。
我们考虑从白边最少的生成树怎么变成白边最多的生成树的。
一条条加入白边,这样就可以将白边的个数慢慢加一也就是生成树的白边的个数一定可以取遍这两个值之间。
代码:
点击查看代码
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;
const int N = 2e6+5;
struct node{
int from,to,opt;
}a[N];
int fa[N],f[N],n,m;
bool cmp1(node a,node b){
return a.opt < b.opt;
}
bool cmp2(node a,node b){
return a.opt > b.opt;
}
int find(int x){
if(fa[x] == x) return x;
return fa[x] = find(fa[x]);
}
int Kruskal(){
for(int i=1; i<=n; i++) fa[i] = i;
int ans = 0;
for(int i=1; i<=m; i++){
if(find(a[i].from) != find(a[i].to)){
int x = find(a[i].from);
int y = find(a[i].to);
fa[x] = y;
ans += a[i].opt;
}
}
return ans;
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
f[1] = 1;
f[0] = 1;
for(int i=2; i<=30; i++){
f[i] = f[i-1] + f[i-2];
}
int t;
scanf("%d",&t);
for(int i=1;i<=t;i++){
printf("Case #%d: ",i);
scanf("%d%d",&n,&m);
for(int i=1; i<=m; i++){
scanf("%d%d%d",&a[i].from,&a[i].to,&a[i].opt);
}
for(int i=1; i<=n; i++) fa[i] = i;
for(int i=1; i<=m; i++){
fa[find(a[i].from)] = find(a[i].to);
}
bool flag = false;
fa[1] = find(fa[1]);
for(int i=2; i<=n; i++){
fa[i] = find(fa[i]);
if(fa[i] != fa[1]){
printf("No\n");
flag = true;
break;
}
}
if(flag) continue;
sort(a+1,a+m+1,cmp1);
int ans1 = Kruskal();
sort(a+1,a+m+1,cmp2);
int ans2 = Kruskal();
for(int i=1; i<=30; i++){
if(ans1 <= f[i] && ans2 >= f[i]){
flag = true;
}
}
if(flag) printf("Yes\n");
else printf("No\n");
}
return 0;
}
总结:
通过对已知的某些题目的再度发掘,从而发现一些新的性质。
比如这题就是类似次小生成树的再度发掘。
[SCOI2011]糖果
题目分析:
这个约束条件显然可以转化为差分约束。
因为我们是求至少是多少也就是要求最长路。
但是这个题我们直接跑最长路会被卡,所以就考虑 tarjan 缩点,将所有必须糖果数相等的点缩成一个点。
这样也可以得到无解的条件:某一条边权大于 \(0\) 的边的两个点被缩到了一个点上。
这样就可以在缩完点后的图上通过 \(\text{DP}\) 求解最长路。
注意:我们每个小朋友至少获得一颗糖果以及注意缩完点后的大小。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6+5;
struct edge{
int nxt,to,val;
edge(){}
edge(int _nxt,int _to,int _val){
nxt = _nxt,to = _to,val = _val;
}
}e[N * 4],g[N];
int n,k,cnt,tot,res,top,col,dp[N],in[N],dis[N],head[N],dfn[N],low[N],st[N],color[N],sz[N];
bool in_que[N];
void add_edge(int from,int to,int val){
e[++cnt] = edge(head[from],to,val);
head[from] = cnt;
}
void tarjan(int now){
dfn[now] = low[now] = ++res;
st[++top] = now;
for(int i = head[now]; i; i = e[i].nxt){
int to = e[i].to;
if(!dfn[to]){
tarjan(to);
low[now] = min(low[now],low[to]);
}
else if(!color[to]){
low[now] = min(low[now],dfn[to]);
}
}
if(low[now] == dfn[now]){
++col;
color[now] = col;sz[col] = 1;
while(st[top] != now){
color[st[top]] = col;
sz[col]++;
top--;
}
top--;
}
}
signed main(){
memset(dp,-0x3f,sizeof(dp));
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%lld%lld",&n,&k);
for(int i=1; i<=k; i++){
int x,a,b;
scanf("%lld%lld%lld",&x,&a,&b);
if(x == 1){
add_edge(a,b,0);
add_edge(b,a,0);
g[++tot] = {a,b,0};
g[++tot] = {b,a,0};
}
else if(x == 2) add_edge(a,b,1),g[++tot] = {a,b,1};
else if(x == 3) add_edge(b,a,0),g[++tot] = {b,a,0};
else if(x == 4) add_edge(b,a,1),g[++tot] = {b,a,1};
else if(x == 5) add_edge(a,b,0),g[++tot] = {a,b,0};
}
for(int i=1; i<=n; i++){
if(!dfn[i]) tarjan(i);
}
memset(head,0,sizeof(head));cnt = 0;
for(int i=1; i<=tot; i++){
int a = color[g[i].nxt];
int b = color[g[i].to];
int v = g[i].val;
if(a == b && v == 1){
printf("-1\n");
return 0;
}
if(a != b){
add_edge(a,b,v);
in[b]++;
}
}
queue<int> q;
for(int i=1; i<=col; i++){
if(!in[i]){
q.push(i);
dp[i] = 1;
}
}
while(!q.empty()){ //注意因为是至少所以应该跑最长路
int now = q.front();q.pop();
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
dp[to] = max(dp[to],dp[now] + e[i].val);
in[to]--;
if(!in[to]) q.push(to);
}
}
int ans = 0;
for(int i=1; i<=col; i++){
ans += sz[i] * dp[i];
}
printf("%lld\n",ans);
return 0;
}
BZOJ2654. tree
题目分析:
我们显然可以通过二分白边的惩罚值来控制白边的个数。
惩罚值也就是给所有的白边加上的一个值。
如果白边个数太多那么就将惩罚值变大,否则则变小。
每次二分结束跑一次最小生成树就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
#define PII pair<int,int>
const int N = 2e6+5;
const int INF = 1e9+5;
struct edge{
int from,to,val,opt;
}a[N];
int fa[N],n,m,k;
bool cmp(edge a,edge b){
if(a.val == b.val) return a.opt < b.opt;
return a.val < b.val;
}
int find(int x){
if(fa[x] == x) return x;
return fa[x] = find(fa[x]);
}
PII Kruskal(int v){
for(int i=1; i<=m; i++)
if(a[i].opt == 0) a[i].val += v;
for(int i=1; i<=n; i++) fa[i] = i;
PII ans = {0,0};
sort(a+1,a+m+1,cmp);
for(int i=1; i<=m; i++){
if(find(a[i].from) != find(a[i].to)){
fa[find(a[i].from)] = find(a[i].to);
ans.first += a[i].opt == 0;
ans.second += a[i].val;
}
}
for(int i=1; i<=m; i++)
if(a[i].opt == 0) a[i].val -= v;
return ans;
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%lld%lld%lld",&n,&m,&k);
for(int i=1; i<=m; i++){
scanf("%lld%lld%lld%lld",&a[i].from,&a[i].to,&a[i].val,&a[i].opt);
a[i].from++;a[i].to++;
}
int l = -1005,r = 1005;
int res = INF;
while(l <= r){
int mid = (l + r)>>1;
PII ans = Kruskal(mid);
if(ans.first >= k){
res = ans.second - k * mid;
l = mid + 1;
}
else if(ans.first < k){
r = mid - 1;
}
}
printf("%lld\n",res);
return 0;
}
9.12
[SCOI2013]摩托车交易
题目分析:
我们考虑一种贪心策略:能买则买,能卖则卖。
也就是我们在每一处都尽可能地买尽可能地卖,而不考虑接下来的限制。
其实我们发现哪怕接下来有限制而导致必须丢弃黄金,我们也可以理解为在一开始少买了一些,所以这样做是正确的。
我们会发现限制也就是两条边的路径上的最短的边,我们要让这条边最大,也就是一棵最大瓶颈生成树,而最大生成树一定是最大瓶颈生成树,所以建一棵最大生成树然后用倍增求解限制就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int INF = 1e18+7;
const int N = 400000;
struct edge{
int from,to,nxt,val;
edge(){}
edge(int _nxt,int _to,int _val){
nxt = _nxt,to = _to,val = _val;
}
}b[N],e[N];
inline int read(){
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
bool cmp(edge l,edge r){
return l.val > r.val;
}
int n,m,q,tot,cnt,ans[N],limit[N],opt[N],a[N],head[N],f[N],dep[N],fa[N][24],mn[N][24];
int find(int x){
if(f[x] == x) return x;
return f[x] = find(f[x]);
}
void add_edge(int from,int to,int val){
e[++cnt] = edge(head[from],to,val);
head[from] = cnt;
}
void Kruskal(){
for(int i=1; i<=n; i++) f[i] = i;
sort(b+1,b+tot+1,cmp);
for(int i = 1; i<=tot; i++){
if(find(b[i].from) != find(b[i].to)){
f[find(b[i].from)] = find(b[i].to);
add_edge(b[i].from,b[i].to,b[i].val);
add_edge(b[i].to,b[i].from,b[i].val);
// printf("%lld %lld %lld\n",b[i].from,b[i].to,b[i].val);
}
}
}
void dfs(int now,int fath){
for(int i = head[now]; i; i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
dep[to] = dep[now] + 1;fa[to][0] = now;mn[to][0] = e[i].val;
dfs(to,now);
}
}
void pre_lca(){
for(int j=1; j<=22; j++){
for(int i=1; i<=n; i++){
fa[i][j] = fa[fa[i][j-1]][j-1];
mn[i][j] = min(mn[i][j-1],mn[fa[i][j-1]][j-1]);
}
}
}
int get_lca(int x,int y){
int ans = INF;
if(dep[x] > dep[y]) swap(x,y);
for(int i = 22; i>=0; i--){
if(dep[fa[y][i]] >= dep[x]){
ans = min(ans,mn[y][i]);
y = fa[y][i];
}
}
if(x == y) return ans;
for(int i = 22; i>=0; i--){
if(fa[x][i] != fa[y][i]){
ans = min(ans,min(mn[x][i],mn[y][i]));
x = fa[x][i];
y = fa[y][i];
}
}
ans = min(ans,min(mn[x][0],mn[y][0]));
return ans;
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
n = read();m=read();q=read();
for(int i=1; i<=n; i++) opt[i]=read();
for(int i=1; i<=n; i++) limit[i]=read();
for(int i=1; i<=m; i++){
++tot;
b[tot].from=read();b[tot].to=read();b[tot].val=read();
}
for(int i=1; i<=q; i++) a[i]=read();
for(int i=1; i<q; i++){
++tot;b[tot].from = a[i],b[tot].to=a[i+1],b[tot].val=INF;
}
Kruskal();
dep[1] = 1;
dfs(1,-1);pre_lca();
int now = opt[1],sum = 0;
for(int i=2; i<=n+1; i++){
if(limit[now] > 0) sum += limit[now];
else{
if(-limit[now] <= sum){
sum -= -limit[now];
printf("%lld\n",-limit[now]);
}
else{
printf("%lld\n",sum);
sum = 0;
}
}
// if(f[now] != f[opt[i]]){
// for(int j=i; j<=n; j++){
// if(limit[opt[j]] < 0){
// printf("0\n");
// }
// }
// break;
// }
int res = get_lca(now,opt[i]);
sum = min(sum,res);now = opt[i];
}
return 0;
}
CF160D. Edges in MST
题目分析:
我们考虑先建出一棵最小生成树。
我们可以假设以下为题目中的三种情况:
- 一定在所有最小生成树上
- 一定在某个最小生成树上
- 一定不在最小生成树上
我们先考虑对于非树边进行判断:
对于非树边我们判断将他加入之后会让最小生成树的值怎么变化,也就是比较它与加上它之后形成的环上的最大值,依旧是分情况讨论。
- 等于:意味着加上这条边之后还是一棵最小生成树,也就是这条边一定在某个最小生成树上
- 大于:意味着加入这条边之后生成树权值和一定变大,一定不是最小生成树,所以不可能在最小生成树上
- 小于:不可能,这样加入这条边之后最小生成树的权值和就会变小。
考虑对于树边进行判断,我们记 \(v_i\) 表示某一条非树边使得加入这条非树边之后 \(i\) 这条边在环上,并且这条非树边的权值最小的权值。换句话说也就是可以替换 \(i\) 的边的最大的非树边的权值:
- \(v_i\) 大于:显然这条边必须存在,因为没有边可以代替它。
- \(v_i\) 小于:与最小生成树相冲突。
- \(v_i\) 等于:必然不存在于所有的最小生成树中,因为可以用这一条边代替。
具体维护可以使用树链剖分和倍增。
代码:
点击查看代码
#include<bits/stdc++.h>
#define PII pair<int,int>
using namespace std;
const int N = 2e5+5;
const int M = 2e5+5;
const int INF = 1e9+5;
struct edge{
int nxt,to,val,id;
edge(){}
edge(int _nxt,int _to,int _val,int _id){
nxt = _nxt,to = _to,val = _val,id = _id;
}
}e[M],a[M];
int n,m,cnt,tot,res,head[N],top[N],f[N],sz[N],son[N],dep[N],fa[N][30],mx[N][30],mn[4 * N],dfn[N],val[N],id[N];
bool flag[N];
string ans[N];
void add_edge(int from,int to,int val,int id){
e[++cnt] = edge(head[from],to,val,id);
head[from] = cnt;
}
bool cmp(edge a,edge b){
return a.val < b.val;
}
//最小生成树---------------------------------------------------------------------
int find(int x){
if(f[x] == x) return x;
return f[x] = find(f[x]);
}
void Kruskal(){
sort(a+1,a+m+1,cmp);
for(int i=1; i<=n; i++) f[i] = i;
for(int i=1; i<=m; i++){
if(find(a[i].nxt) != find(a[i].to)){
f[find(a[i].nxt)] = find(a[i].to);
flag[i] = true;
}
}
}
//------------------------------------------------------------------
//倍增+树剖预处理------------------------------------------------------------------
void dfs1(int now,int fath){
fa[now][0] = fath;
dep[now] = dep[fath] + 1;
sz[now] = 1;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
dfs1(to,now);
mx[to][0] = e[i].val;
sz[now] += sz[to];
if(sz[to] > sz[son[now]]) son[now] = to;
}
}
void dfs2(int now,int topf){
dfn[now] = ++tot;id[tot] = now;
val[tot] = mx[now][0];top[now] = topf;
if(son[now]) dfs2(son[now],topf);
for(int i = head[now]; i; i = e[i].nxt){
int to = e[i].to;
if(to == son[now] || to == fa[now][0]) continue;
dfs2(to,to);
}
}
void pre_lca(){
for(int i=1; i<=25; i++){
for(int j=1; j<=n; j++){
fa[j][i] = fa[fa[j][i-1]][i-1];
mx[j][i] = max(mx[j][i-1],mx[fa[j][i-1]][i-1]);
}
}
}
PII get_lca(int x,int y){
if(dep[x] > dep[y]) swap(x,y);
int ans = 0;
for(int i=25; i>=0; i--){
if(dep[fa[y][i]] >= dep[x]){
ans = max(ans,mx[y][i]);
y = fa[y][i];
}
}
if(x == y) return {x,ans};
for(int i=25; i>=0; i--){
if(fa[x][i] != fa[y][i]){
ans = max(ans,max(mx[x][i],mx[y][i]));
x = fa[x][i];y = fa[y][i];
}
}
ans = max(ans,max(mx[x][0],mx[y][0]));
return {fa[x][0],ans};
}
//------------------------------------------------------------------
//线段树-------------------------------------------------------
void build(int now,int now_l,int now_r){
mn[now] = INF;
if(now_l == now_r) return;
int mid = (now_l + now_r)>>1;
build(now<<1,now_l,mid);
build(now<<1|1,mid+1,now_r);
}
int query(int now,int now_l,int now_r,int pos){
res = min(res,mn[now]);
if(now_l == now_r) return res;
int mid = (now_l + now_r)>>1;
if(pos <= mid) return query(now<<1,now_l,mid,pos);
else return query(now<<1|1,mid+1,now_r,pos);
}
void update(int now,int now_l,int now_r,int l,int r,int v){
if(l <= now_l && r >= now_r){
mn[now] = min(mn[now],v);
return;
}
int mid = (now_l + now_r)>>1;
if(l <= mid) update(now<<1,now_l,mid,l,r,v);
if(r > mid) update(now<<1|1,mid+1,now_r,l,r,v);
}
void change(int x,int y,int v){
int lca = get_lca(x,y).first;
while(top[x] != top[lca]){
update(1,1,tot,dfn[top[x]],dfn[x],v);
x = fa[top[x]][0];
}
if(dfn[lca] < dfn[x]) update(1,1,tot,dfn[lca]+1,dfn[x],v);
while(top[y] != top[lca]){
update(1,1,tot,dfn[top[y]],dfn[y],v);
y = fa[top[y]][0];
}
if(dfn[lca] < dfn[y]) update(1,1,tot,dfn[lca]+1,dfn[y],v);
}
//---------------------------------------------------------------------
//统计答案-------------------------------------------------------------
void dfs3(int now,int fath){
for(int i=head[now]; i; i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
dfs3(to,now);
res = INF;
int h = query(1,1,tot,dfn[to]);
if(h > e[i].val) ans[e[i].id] = "any";
else ans[e[i].id] = "at least one";
}
}
//---------------------------------------------------------------------
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1; i<=m; i++){
scanf("%d%d%d",&a[i].nxt,&a[i].to,&a[i].val);
a[i].id = i;
}
Kruskal();
for(int i=1; i<=m; i++){
if(flag[i]){
add_edge(a[i].nxt,a[i].to,a[i].val,a[i].id);
add_edge(a[i].to,a[i].nxt,a[i].val,a[i].id);
}
}
dfs1(1,0);
dfs2(1,1);
pre_lca();
build(1,1,tot);
for(int i=1; i<=m; i++){
if(!flag[i]){
int mx = get_lca(a[i].nxt,a[i].to).second;
if(a[i].val == mx) ans[a[i].id] = "at least one";
else ans[a[i].id] = "none";
change(a[i].nxt,a[i].to,a[i].val);
// printf("%d %d %d\n",a[i].nxt,a[i].to,a[i].val);
}
}
dfs3(1,0);
for(int i=1; i<=m; i++){
cout<<ans[i]<<endl;
}
return 0;
}
CF1728C Digital Logarithm
题目分析:
我们可以发现一个性质:对于任意数取 \(\lg\) 之后都会变成个位数。
所以就考虑将原序列相等的去掉,然后全部取 \(\lg\) 再去掉相等的,然后再将剩下的全部变成 \(1\) 就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 3e5+5;
int cnt1,cnt2,a[N],b[N],a2[N],b2[N],tot1[20],tot2[20];
bool flag[N];
int get_len(int x){
int ans = 0;
while(x){
ans ++;
x/=10;
}
return ans;
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int t;
scanf("%d",&t);
while(t--){
map<int,int> mp;
int n;
scanf("%d",&n);
for(int i=1; i<=n; i++){
scanf("%d",&a[i]);
mp[a[i]]++;
}
for(int i=1; i<=n; i++){
scanf("%d",&b[i]);
if(mp[b[i]] > 0){
mp[b[i]]--;
flag[i] = true;
}
}
int ans = 0;
for(int i=1; i<=n; i++){
if(mp[a[i]] > 0){
mp[a[i]]--;
if(a[i] >= 10){
a[i] = get_len(a[i]);
ans++;
}
a2[++cnt1] = a[i];
tot1[a[i]]++;
}
}
for(int i=1; i<=n; i++){
if(!flag[i]){
if(b[i] >= 10){
b[i] = get_len(b[i]);
ans++;
}
b2[++cnt2] = b[i];
tot2[b[i]]++;
}
}
for(int i=2; i<=9; i++){
ans += abs(tot1[i] - tot2[i]);
}
printf("%d\n",ans);
memset(tot1,0,sizeof(tot1));memset(tot2,0,sizeof(tot2));cnt1 = cnt2 = 0;
for(int i=1; i<=n; i++) flag[i] = false;
}
return 0;
}
CF1728D Letter Picking
题目分析:
显然我们可以考虑逆向思维。
设 \(dp[l][r]\) 表示 \([l,r]\) 内的全部选好的结果,其中 \(-1\) 代表 \(\text{Alice}\),\(0\) 代表平局,\(1\) 代表 \(\text{Bob}\)
因为我们这相当于两个人依次操作一次算一轮,所以就是按两个来进行转移。
也就是从 \([l+2,r],[l+1,r-1],[l,r-2]\) 转移过来。
我们可以发现数值越小对于先手越有利,数值越大对于后手越有利,所以按照这个取最大值或者最小值然后转移就好了。
代码里相当于分先手先取哪一个来讨论的。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 3e3+5;
int dp[N][N];
char s[N];
int cmp(char l,char r){
if(l < r) return -1;
if(l > r) return 1;
return 0;
}
int main(){
int t;
scanf("%d",&t);
while(t--){
scanf("%s",s+1);
int n = strlen(s+1);
for(int len=2; len<=n; len+=2){
for(int l=1; l + len - 1 <= n; l++){
int r = l + len - 1;
dp[l][r] = 1;
int tmp = -1;
if(dp[l+1][r-1] != 0) tmp = max(tmp,dp[l+1][r-1]); //考虑先手选择左边的
else tmp = max(tmp,cmp(s[l],s[r])); //都选择各自最优的决策
if(dp[l+2][r] != 0) tmp = max(tmp,dp[l+2][r]);
else tmp = max(tmp,cmp(s[l],s[l+1]));
dp[l][r] = min(dp[l][r],tmp);
tmp = -1;
if(dp[l+1][r-1] != 0) tmp=max(tmp,dp[l+1][r-1]); //考虑先手选择右边的
else tmp = max(tmp,cmp(s[r],s[l]));
if(dp[l][r-2] != 0) tmp = max(tmp,dp[l][r-2]);
else tmp = max(tmp,cmp(s[r],s[r-1]));
dp[l][r] = min(dp[l][r],tmp);
//只要存在一种情况先手赢,那么肯定赢,这里的 max min 也就是这么妙 QwQ
}
}
if(dp[1][n] == -1) printf("Alice\n");
else if(dp[1][n] == 0) printf("Draw\n");
else printf("Bob\n");
}
return 0;
}
总结:
正难则反易。对于很多题目逆着考虑都会简单很多。
CF733F Drivers Dissatisfaction
题目分析:
我们发现一点性质:我们的减少权值一定是盯着一条边一直删最优。可以通过分类讨论证明。
这样我们就相当于先建出一棵最小生成树,然后枚举每一条边判断减少权值会造成什么影响。
对于树边就是最小生成树的权值直接减。
对于非树边就是去替代最小生成树上的最大权值的边。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
struct edge{
long long from,to,w,c,pos;
edge(long long a1=0,long long a2=0,long long a3=0,long long a4=0,long long a5=0){
from=a1;to=a2;w=a3;c=a4;pos=a5;
}
};
long long w[200005],c[200005],deep[200005],n,m,s,f[200005],fa[200005][30],mx[200005][30][2],cnt,ans=1e18;
bool in_tree[200005];
vector<edge> v[200005],tree[200005];
edge a[200005];
bool cmp(edge l,edge r){
return l.w < r.w;
}
long long find(long long x){
if(f[x] != x)
return f[x] = find(f[x]);
return x;
}
void kruskal(long long s){
sort(a+1,a+m+1,cmp);
for(long long i=1; i<=n; i++){
f[i]=i;
}
for(long long i=1; i<=m; i++){
long long from=a[i].from,to=a[i].to;
if(find(from) != find(to)){
f[find(from)]=find(to);
cnt+=a[i].w;
tree[from].push_back(edge(from,to,a[i].w,a[i].c,a[i].pos)); //建最小生成树
tree[to].push_back(edge(to,from,a[i].w,a[i].c,a[i].pos));
in_tree[a[i].pos]=true;
// printf("最小生成树包含:%d - %d 权值为%d\n",from,to,a[i].w);
}
}
}
void dfs(long long x,long long g){
for(long long i=0; i<tree[x].size(); i++){ //转有根树
long long to=tree[x][i].to;
if(to == g)
continue;
fa[to][0]=x;
deep[to]=deep[x]+1;
mx[to][0][1]=tree[x][i].w;
mx[to][0][0]=tree[x][i].pos;
dfs(to,x);
}
}
void chuli(){
for(long long i=1; i<=25; i++){
for(long long j=1; j<=n; j++){
fa[j][i]=fa[fa[j][i-1]][i-1];
if(mx[j][i-1][1] > mx[fa[j][i-1]][i-1][1]){
mx[j][i][1]=mx[j][i-1][1];
mx[j][i][0]=mx[j][i-1][0];
}
if(mx[j][i-1][1] <= mx[fa[j][i-1]][i-1][1]){
mx[j][i][1]=mx[fa[j][i-1]][i-1][1];
mx[j][i][0]=mx[fa[j][i-1]][i-1][0];
}
}
}
}
pair<long long,long long> get_lca(long long x,long long y){
long long k=-1000000,kn=0;
if(deep[x] > deep[y])
swap(x,y);
for(long long i=25; i>=0; i--){
if(deep[fa[y][i]] >= deep[x]){
if(mx[y][i][1] > k){
k=mx[y][i][1];
kn=mx[y][i][0];
}
y=fa[y][i];
}
}
if(x == y)
return make_pair(k,kn);
for(long long i=25; i>=0; i--){
if(fa[y][i] != fa[x][i]){
if(max(mx[x][i][1],mx[y][i][1]) > k){
if(mx[x][i][1] > mx[y][i][1]){
k=mx[x][i][1];
kn=mx[x][i][0];
}
else{
k=mx[y][i][1];
kn=mx[y][i][0];
}
}
y=fa[y][i];
x=fa[x][i];
}
}
if(max(mx[y][0][1],mx[x][0][1]) > k){
if(mx[y][0][1] > mx[x][0][1]){
k=mx[y][0][1];
kn=mx[y][0][0];
}
else{
k=mx[x][0][1];
kn=mx[x][0][0];
}
}
return make_pair(k,kn);
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
cin>>n>>m;
for(long long i=1; i<=m; i++){
cin>>a[i].w;
}
for(long long i=1; i<=m; i++){
cin>>a[i].c;
}
for(long long i=1; i<=m; i++){
long long from,to;
cin>>from>>to;
a[i].from=from;
a[i].to=to;
a[i].pos=i;
v[from].push_back(edge(from,to,w[i],c[i],-1));
v[to].push_back(edge(to,from,w[i],c[i],-1));
}
cin>>s;
kruskal(1);
dfs(1,-1);
chuli();
long long ansn=0,shan_bian=0;
bool flag=false;
for(long long i=1; i<=m; i++){
// printf("第%lld边:%lld - %lld 权值为:%lld\n",i,a[i].from,a[i].to,a[i].w);
}
for(long long i=1; i<=m; i++){
long long from=a[i].from,to=a[i].to;
if(in_tree[a[i].pos]){
long long h=cnt-(s/a[i].c);//在最小生成树上就直接减就完了
if(ans > h){
ansn=i; //ansn代表我选择减的边
ans=h;
flag=true; //flag记录该边是否在最小生成树上
}
// printf("第%lld条边在最小生成树上,得到的答案为:%lld\n",i,(cnt-(s/a[i].c)));
}
else{ //不在最小生成树上,用这条边替代形成的环上的最长边
pair<long long,long long> mx_lca=get_lca(from,to);
long long h=cnt-mx_lca.first+(a[i].w-(s/a[i].c));
if(ans > h){
ans=h;
ansn=i;
flag=false;
shan_bian=mx_lca.second;
}
// printf("第%lld条边不在最小生成树上,其最长边为:%lld,得到的答案为:%lld\n",i,mx_lca,(cnt-mx_lca.first+(a[i].w-(s/a[i].c))));
}
}
cout<<ans<<endl;
if(flag){
for(long long i=1; i<=m; i++){
if(in_tree[a[i].pos]){
if(ansn == i)
printf("%lld %lld\n",a[i].pos,a[i].w-(s/a[i].c)); //在最小生成树上则就将那一条边改改就好
else
printf("%lld %lld\n",a[i].pos,a[i].w);
}
}
}
else{
for(long long i=1; i<=m; i++){
if(in_tree[a[i].pos]){
if(a[i].pos == shan_bian)
continue;
printf("%lld %lld\n",a[i].pos,a[i].w);
}
}
printf("%lld %lld\n",a[ansn].pos,a[ansn].w-(s/a[ansn].c));
}
return 0;
}
HDU1506 Largest Rectangle in a Histogram
题目分析:
这题应该就是单调栈的经典应用。
我们找到每个数前面第一个小于他的位置 \(l[i]\) 和后面第一个小于他的位置 \(r[i]\)
那么我们的答案就是 \(\max(h[i] \times (r[i] - l[i] - 1))\)
我们对于 \(l[i],r[i]\) 的求解可以维护一个单调递增的单调栈,然后分别从前到后以及从后到前加入数,根据插入的位置就可以得到。
代码:
点击查看代码
#include<cstdio>
#include<algorithm>
#include<cmath>
#define int long long
using namespace std;
const int N = 2e6+5;
int a[N],st[N],pre[N],suf[N];
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
while(1){
int n;
scanf("%lld",&n);
if(n == 0) break;
for(int i=1; i<=n; i++) scanf("%lld",&a[i]);
int l = 1,r = 0,ans=0;
for(int i=1; i<=n; i++){ //找到小于 a[i] 的第一个位置
while(l <= r && a[st[r]] >= a[i]) r--;
if(l > r) pre[i] = 0;
else pre[i] = st[r];
st[++r] = i;
}
l = 1,r = 0;
for(int i=n; i>=1; i--){ //找到小于 a[i] 的最后一个位置
while(l <= r && a[st[r]] >= a[i]) r--;
if(l > r) suf[i] = n + 1;
else suf[i] = st[r];
st[++r] = i;
}
for(int i=1; i<=n; i++){
ans = max(ans,a[i] * (suf[i] - pre[i] - 1)); //注意这里是不包含两个端点的
}
printf("%lld\n",ans);
}
return 0;
}
HDU5945 Fxx and game
题目分析:
我们考虑设 \(dp[i]\) 表示将 \(x\) 变为 \(i\) 的最小步数。
考虑第一种转移也就是 \(dp[i] = \min_{j=i}^{i+t} dp[j] + 1\) 可以考虑使用单调队列优化。
对于第二种转移就是 \(dp[i] = dp[i \times k] + 1\),注意这里会爆 \(\text{int}\)。
代码:
点击查看代码
#include<cmath>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define int long long
using namespace std;
const int N = 2e6+5;
int dp[N],st[N];
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int h;
scanf("%lld",&h);
while(h--){
memset(dp,0x3f,sizeof(dp));
int x,k,t;
scanf("%lld%lld%lld",&x,&k,&t);
if(t == 0){
int ans = 0;
while(x != 1){
x /= k;
ans++;
}
printf("%lld\n",ans);
continue;
}
if(k == 0){
if((x-1) % t == 0) printf("%lld\n",(x-1) / t);
else printf("%lld\n",((x-1) / t) + 1);
continue;
}
dp[x] = 0;st[1] = x;
int l=1,r=1;
for(int i=x-1; i>=1; i--){
while(l <= r && st[l] > i + t) l++;
if(i != x){
dp[i] = dp[st[l]] + 1;
if(i * k <= x) dp[i] = min(dp[i],dp[i * k] + 1);
}
while(l <= r && dp[st[r]] >= dp[i]) r--;
st[++r] = i;
}
printf("%lld\n",dp[1]);
}
return 0;
}
总结:
可以将 DP 的转移分开,分为几块,对于每一块内发现一些性质然后优化。
LightOJ1424 New Land
题目分析:
我们显然可以预处理出 \(ans[i][j]\) 表示从 \((i,j)\) 向上最多可以扩展多长的 \(0\)。
下面就是枚举矩阵最下面的一行是什么,然后问题就可以转化为每个数找到左边以及右边小于它的第一个数的位置。
也就就是 HDU1506 的方法了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 3e3+5;
int a[N][N],ans[N][N],st[N],L[N],R[N];
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int t;
scanf("%d",&t);
for(int p=1; p<=t; p++){
printf("Case %d: ",p);
int n,m;
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++){
for(int j=1; j<=m; j++){
char c;
cin>>c;
a[i][j] = c - '0';
}
}
for(int i=1; i<=m; i++){
for(int j=1; j<=n; j++){
if(a[j][i] == 1) ans[j][i] = 0;
else ans[j][i] = ans[j-1][i] + 1;
}
}
int res = 0;
for(int i=1; i<=n; i++){
int l = 1,r = 0;
for(int j=1; j<=m; j++){
while(l <= r && ans[i][st[r]] >= ans[i][j]) r--;
if(l > r) L[j] = 0;
else L[j] = st[r];
st[++r] = j;
}
l=1,r=0;
for(int j=m; j>=1; j--){
while(l <= r && ans[i][st[r]] >= ans[i][j]) r--;
if(l > r) R[j] = m + 1;
else R[j] = st[r];
st[++r] = j;
}
for(int j=1; j<=m; j++){
res = max(res,(R[j] - L[j] - 1) * ans[i][j]);
}
}
printf("%d\n",res);
}
return 0;
}
9.13
CF510C Fox And Names
题目分析:
我们显然可以通过相邻两个字符串来判断两个字符的字典序大小,并且可以根据这个字典序从小到大连边。
这样就只需要跑一边拓扑序,把跑到的输出一下就是答案。
注意:我们需要最小的字典序,所以我们需要用优先队列每一次找到最小的字符然后选择。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e3+5;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N];
int ans[N],tot,cnt,head[N],ru[N];
char s[N][N];
bool vis[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n;
scanf("%d",&n);
for(int i=1; i<=n; i++){
scanf("%s",s[i] + 1);
}
for(int i=1; i<n; i++){
int len1 = strlen(s[i] + 1);
int len2 = strlen(s[i + 1] + 1);
int pos = 1;
while(pos <= min(len1,len2) && s[i][pos] == s[i+1][pos]) pos++;
if(pos == len2 + 1){
printf("Impossible\n");
return 0;
}
if(pos <= len1 && s[i][pos] != s[i+1][pos]){
add_edge(s[i][pos] - 'a',s[i+1][pos] - 'a');
ru[s[i+1][pos] - 'a'] ++;
// cout<<s[i][pos]<<' '<<s[i+1][pos]<<endl;
}
}
priority_queue<int,vector<int>,greater<int> > q;
for(int i=0; i<26; i++){
if(!ru[i]) q.push(i);
}
while(!q.empty()){
int now = q.top();q.pop();vis[now] = true;
ans[++tot] = now;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if((--ru[to]) == 0){
q.push(to);
}
}
}
for(int i=0; i<26; i++){
if(!vis[i]){
printf("Impossible\n");
return 0;
}
}
for(int i=1; i<=tot; i++){
cout<<char(ans[i] + 'a');
}
return 0;
}
CF20C Dijkstra?
题目分析:
就是跑一边 Dijkstra 求最短路,然后求的过程中记录一下上一个点是什么,然后递归输出就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define PII pair<int,int>
using namespace std;
const int N = 2e5+5;
struct edge{
int nxt,to,val;
edge(){}
edge(int _nxt,int _to,int _val){
nxt = _nxt,to = _to,val = _val;
}
}e[N * 2];
int cnt,dis[N],head[N],from[N];
bool vis[N];
void add_edge(int from,int to,int val){
e[++cnt] = edge(head[from],to,val);
head[from] = cnt;
}
void dij(int s){
memset(dis,0x3f,sizeof(dis));memset(vis,false,sizeof(vis));
priority_queue<PII> q;
dis[s] = 0;q.push({-dis[s],s});
while(!q.empty()){
int now = q.top().second;q.pop();
if(vis[now]) continue;
vis[now] = true;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(dis[to] > dis[now] + e[i].val && !vis[to]){
dis[to] = dis[now] + e[i].val;
from[to] = now;
q.push({-dis[to],to});
}
}
}
}
void get_out(int now){
if(from[now]) get_out(from[now]);
printf("%lld ",now);
}
signed main(){
int n,m;
scanf("%lld%lld",&n,&m);
for(int i=1; i<=m; i++){
int from,to,val;
scanf("%lld%lld%lld",&from,&to,&val);
add_edge(from,to,val);
add_edge(to,from,val);
}
dij(1);
if(!from[n]) printf("-1");
else{
get_out(n);
}
return 0;
}
[JLOI2011]飞行路线
题目分析:
因为我们发现 \(k\) 很小,所以就考虑魔改一下 Dijkstra。
设 \(dp[i][j]\) 为 \(s\) 到 \(i\) 有 \(j\) 条免费边的最短路。
那么就在 Dijkstra 里存这个东西,然后每次找一个最小的正常跑最短路就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5+5;
const int INF = 1e18+5;
struct edge{
int nxt,to,val;
edge(){}
edge(int _nxt,int _to,int _val){
nxt = _nxt,to = _to,val = _val;
}
}e[N];
struct node{
int dis,now,use;
node(){}
node(int _dis,int _now,int _use){
dis = _dis,now = _now,use = _use;
}
};
bool operator < (node a,node b){
return a.dis > b.dis;
}
int cnt,s,t,n,m,k,dis[N][15],head[N];
void add_edge(int from,int to,int val){
e[++cnt] = edge(head[from],to,val);
head[from] = cnt;
}
void dij(){
memset(dis,0x3f,sizeof(dis));
priority_queue<node> q;dis[s][0] = 0;
q.push(node(dis[s][0],s,0));
while(!q.empty()){
int now = q.top().now;
int ti = q.top().use;q.pop();
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(dis[to][ti] > dis[now][ti] + e[i].val){
dis[to][ti] = dis[now][ti] + e[i].val;
q.push(node(dis[to][ti],to,ti));
}
if(ti < k && dis[to][ti + 1] > dis[now][ti]){
dis[to][ti + 1] = dis[now][ti];
q.push(node(dis[to][ti+1],to,ti+1));
}
}
}
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%lld%lld%lld",&n,&m,&k);
scanf("%lld%lld",&s,&t);s++;t++;
for(int i=1; i<=m; i++){
int from,to,val;
scanf("%lld%lld%lld",&from,&to,&val);from++;to++;
add_edge(from,to,val);add_edge(to,from,val);
}
dij();
int ans = INF;
for(int i=0; i<=k; i++) ans = min(ans,dis[t][i]);
printf("%lld\n",ans);
return 0;
}
[CodeChef - CLIQUED]Bear and Clique Distances
题目分析:
我们会发现如果直接对于前 \(k\) 个点暴力建边肯定会 T,所以就考虑优化一下。
我们新建一个点,将前 \(k\) 个点向这个点连边权值为 \(x\),同时由这个点向前 \(k\) 个点连边,权值为 \(0\)。这样就很好地解决了这 \(k\) 个点内部之间连边的问题。
对于剩下的就正常跑最短路就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 6e5+5;
struct edge{
int nxt,to,val;
edge(){}
edge(int _nxt,int _to,int _val){
nxt = _nxt,to = _to,val = _val;
}
}e[N];
int cnt,dis[N],head[N];
bool vis[N];
void add_edge(int from,int to,int val){
e[++cnt] = edge(head[from],to,val);
head[from] = cnt;
}
void dij(int s){
memset(dis,0x3f,sizeof(dis));memset(vis,false,sizeof(vis));
priority_queue<pair<int,int> > q;
dis[s] = 0;q.push({-dis[s],s});
while(!q.empty()){
int now = q.top().second;q.pop();
if(vis[now]) continue;
vis[now] = true;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(!vis[to] && dis[to] > dis[now] + e[i].val){
dis[to] = dis[now] + e[i].val;
q.push({-dis[to],to});
}
}
}
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int t;
scanf("%lld",&t);
while(t--){
memset(head,0,sizeof(head));cnt=0;
int n,k,x,m,s;
scanf("%lld%lld%lld%lld%lld",&n,&k,&x,&m,&s);
for(int i=1; i<=m; i++){
int from,to,val;
scanf("%lld%lld%lld",&from,&to,&val);
add_edge(from,to,val);
add_edge(to,from,val);
}
for(int i=1; i<=k; i++){
add_edge(i,n+1,x);
add_edge(n+1,i,0);
}
dij(s);
for(int i=1; i<=n; i++){
printf("%lld ",dis[i]);
}
printf("\n");
}
return 0;
}
POJ1201 Intervals
题目分析:
我们可以设 \(x[i]\) 表示 \(i\) 这个数有没有被选择,\(x[i] = 1\) 表示被选择,\(x[i] = 0\) 表示没有被选择。
我们会发现对于一个区间的限制如果转化到前缀和数组上就相当于对于两个点的限制,所以我们对于 \(x\) 求一个前缀和记为 \(s\)。
我们可以发现对于一个限制 \((a,b,c)\),转化到前缀和数组中就是:
这也就很有差分约束的味道了,那么我们考虑这样约束就够了吗?
其实不够因为还有前缀和数组的限制,也就是下面这两条:
那么稍微转化一下就是:
那么因为我们取大于等于号所以应该跑最长路,最后的 \(s\) 即为答案。
代码:
点击查看代码
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int N = 5e5+5;
struct edge{
int nxt,to,val;
edge(){}
edge(int _nxt,int _to,int _val){
nxt = _nxt,to = _to,val = _val;
}
}e[N];
int cnt,dis[N],head[N];
bool in_que[N];
void add_edge(int from,int to,int val){
e[++cnt] = edge(head[from],to,val);
head[from] = cnt;
}
void spfa(){
memset(dis,-0x3f,sizeof(dis));
queue<int> q;
q.push(0);dis[0] = 0;in_que[0] = true;
while(!q.empty()){
int now = q.front();q.pop();in_que[now] = false;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(dis[to] < dis[now] + e[i].val){
dis[to] = dis[now] + e[i].val;
if(!in_que[to]){
q.push(to);
in_que[to] = true;
}
}
}
}
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n;
scanf("%d",&n);
int mx = 0;
for(int i=1; i<=n; i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add_edge(u,v+1,w);
// printf("%d %d %d\n",v+1,u,w);
mx = max(mx,max(u,v+1));
}
for(int i=0; i<=mx; i++){
add_edge(i,i+1,0);
add_edge(i+1,i,-1);
// printf("%d %d %d\n",i,i+1,0);
// printf("%d %d %d\n",i+1,i,-1);
}
spfa();
printf("%d\n",dis[mx]);
return 0;
}
总结:
对于一些区间上的问题,如果发现没有什么思路,可以考虑差分以及前缀和之后进行考虑
9.14
[COCI2016-2017#5] Poklon
题目分析:
考虑使用莫队。
显然我们可以记录一个数的出现次数,加入删除的时候判断 \(2\) 就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 6e5+5;
struct Query{
int l,r,id;
}q[N];
int tot,len,res,ans[N],cnt[N],a[N],b[N];
inline int read(){
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
int get(int x){
return x / len;
}
bool cmp(Query a,Query b){
if(get(a.l) != get(b.l)) return get(a.l) < get(b.l);
return a.r < b.r;
}
void Add(int v){
cnt[v]++;
if(cnt[v] == 2) res++;
if(cnt[v] == 3) res--;
}
void Delet(int v){
cnt[v]--;
if(cnt[v] == 2) res++;
if(cnt[v] == 1) res--;
}
int main(){
int n,m;
n=read();m=read();
for(int i=1; i<=n; i++){
a[i] = read();
b[++tot] = a[i];
}
sort(b+1,b+tot+1);
tot = unique(b+1,b+tot+1) - b - 1;
for(int i=1; i<=n; i++){
a[i] = lower_bound(b+1,b+tot+1,a[i]) - b;
}
for(int i=1; i<=m; i++){
q[i].l = read();q[i].r = read();
q[i].id = i;
}
len = sqrt(n);
sort(q+1,q+m+1,cmp);
int now_l=1,now_r=0;
for(int i=1; i<=m; i++){
int l = q[i].l;
int r = q[i].r;
while(now_l > l) Add(a[--now_l]);
while(now_r < r) Add(a[++now_r]);
while(now_l < l) Delet(a[now_l++]);
while(now_r > r) Delet(a[now_r--]);
ans[q[i].id] = res;
}
for(int i=1; i<=m; i++){
printf("%d\n",ans[i]);
}
return 0;
}
[BJWC2018]基础匹配算法练习题
题目分析:
我们可以发现一个神奇的性质:
将 \(A_i\) 排序之后,每个 \(B_i\) 可以连接的就是 \(A\) 对应的一个前缀。
对于这个题我们显然可以考虑莫队。
对于最大的匹配我们显然可以考虑使用线段树优化这个过程。
具体就是在线段树中记录:\(sz,sum,ans\) 分别代表区间大小、区间内 \(B\) 的数量、区间内匹配的数量。
这样的话区间和并就是用右区间剩下的 \(B\) 与左区间剩下的 \(A\) 匹配,形成新的匹配。
而为了使得我们的性质可以满足,我们每一次插入或删除一个 \(B\),都在线段树上二分一个他能匹配的最远的距离插入,然后向上转移就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e5+5;
struct Query{
int l,r,id;
}q[N];
int len,tmp[N],a[N],b[N],sz[4 * N],ans[4 * N],sum[4 * N],z;
inline int read(){
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
int get(int x){
return x / len;
}
bool cmp(Query a,Query b){
if(get(a.l) != get(b.l)) return get(a.l) < get(b.l);
if(get(a.l) & 1) return a.r < b.r;
return a.r > b.r;
// return a.r < b.r;
}
void pushup(int now){
sz[now] = sz[now<<1] + sz[now<<1|1];
sum[now] = sum[now<<1] + sum[now<<1|1];
ans[now] = ans[now<<1] + ans[now<<1|1];
ans[now] += max(min(sz[now<<1] - ans[now<<1],sum[now<<1|1] - ans[now<<1|1]),0ll);
//min(左区间 A 的数量,右区间 B 的数量)
}
void build(int now,int now_l,int now_r){
if(now_l == now_r){
sz[now] = 1;
sum[now] = 0;
ans[now] = 0;
return;
}
int mid = (now_l + now_r)>>1;
build(now<<1,now_l,mid);build(now<<1|1,mid+1,now_r);
pushup(now);
}
void change(int now,int now_l,int now_r,int val,int c){
if(now_l == now_r){
if(val + a[now_l] > z) return;
if(sum[now] == 0) ans[now] = 1;
sum[now] += c;
if(sum[now] == 0) ans[now] = 0;
return;
}
int mid = (now_l + now_r)>>1;
if(val + a[mid+1] <= z) change(now<<1|1,mid+1,now_r,val,c);
else change(now<<1,now_l,mid,val,c);
pushup(now);
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n,m;
n=read();m=read();z=read();
for(int i=1; i<=n; i++){
a[i]=read();
}
sort(a+1,a+n+1);
build(1,1,n);
for(int i=1; i<=m; i++){
b[i]=read();
}
int Q;
Q=read();
for(int i=1; i<=Q; i++){
q[i].l=read();q[i].r=read();
q[i].id = i;
}
len = sqrt(m);
sort(q+1,q+Q+1,cmp);
int now_l = 1,now_r = 0;
for(int i=1; i<=Q; i++){
int l = q[i].l;
int r = q[i].r;
while(now_l > l) change(1,1,n,b[--now_l],1);
while(now_r < r) change(1,1,n,b[++now_r],1);
while(now_l < l) change(1,1,n,b[now_l++],-1);
while(now_r > r) change(1,1,n,b[now_r--],-1);
tmp[q[i].id] = ans[1];
}
for(int i=1; i<=Q; i++){
printf("%lld\n",tmp[i]);
}
return 0;
}
总结:
要合理的分析复杂度,就比如本题莫队的复杂度就是 \(O(m\sqrt{m})\)
也要合理的结合多个知识点解决问题。
[SNOI2017]一个简单的询问
题目分析:
我一开始感觉可以直接两个莫队维护两个区间,但是显然只能开一个莫队维护其中一个区间另一个只能乱搞,显然过不去。
那么我们就考虑将这个式子拆开:
所以我们其实这就可以拆成四个询问:
那么我们直接就将一个询问拆成了四个,全部搞在一起跑莫队就好了。
这时莫队的 \(l,r\) 就是维护的 \([1,l]\) 和 \([1,r]\) 之间的答案了。
一定不要与一般莫队维护的信息搞混了
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e5+5;
struct Query{
int l,r,id,opt;
Query(){}
Query(int _l,int _r,int _id,int _opt){
l = _l,r = _r,id = _id,opt = _opt;
}
}q[N];
int len,n,m,tot,res,now_l,now_r,ans[N],a[N],cntl[N],cntr[N];
//cntl 和 cntr 分别记录 l,r 这两个前缀的数的出现次数
int get(int x){
return x / len;
}
bool cmp(Query a,Query b){
if(get(a.l) != get(b.l)) return get(a.l) < get(b.l);
return a.r < b.r;
}
void movel(int opt){ //需要注意:我们这两个都是独立的,都是维护的自己的前缀
if(opt == 1){
now_l++;
cntl[a[now_l]]++;
res += cntr[a[now_l]];
}
else if(opt == -1){
cntl[a[now_l]]--;
res -= cntr[a[now_l]];
now_l--;
}
}
void mover(int opt){
if(opt == -1){
cntr[a[now_r]]--;
res -= cntl[a[now_r]];
now_r--;
}
else if(opt == 1){
now_r++;
cntr[a[now_r]]++;
res += cntl[a[now_r]];
}
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n;
scanf("%lld",&n);
for(int i=1; i<=n; i++){
scanf("%lld",&a[i]);
}
int m;
scanf("%lld",&m);
for(int i=1; i<=m; i++){
int l1,r1,l2,r2;
scanf("%lld%lld%lld%lld",&l1,&r1,&l2,&r2);
q[++tot] = {r1,r2,i,1}; //考虑差分
q[++tot] = {r1,l2-1,i,-1};
q[++tot] = {l1-1,r2,i,-1};
q[++tot] = {l1-1,l2-1,i,1};
}
for(int i=1; i<=tot; i++){//很关键的一步,不要忽略
if(q[i].l > q[i].r)
swap(q[i].l,q[i].r);
}
len = sqrt(n);
sort(q+1,q+tot+1,cmp);
now_l = 0,now_r = 0;
for(int i=1; i<=tot; i++){
int l = q[i].l,r = q[i].r;
while(now_l > l) movel(-1);
while(now_r < r) mover(1);
while(now_l < l) movel(1);
while(now_r > r) mover(-1);
ans[q[i].id] += res * q[i].opt;
}
for(int i=1; i<=m; i++){
printf("%lld\n",ans[i]);
}
return 0;
}
总结:
将询问拆开,变得统一好做。
要善于变化,不能只局限于一般莫队。
[CQOI2018]异或序列
题目分析:
对于这类的题显然需要维护前缀异或和。
那么我们维护之后对于询问 \([l,r]\) 其实就是询问有多少个数异或为 \(k\)。
我们知道 \(x \oplus y = k\) 相当于 \(x \oplus k = y\),所以我们知道了一个数就能知道与他异或为 \(k\) 的另一个数是什么。
那么就开个桶维护一下就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e5+5;
struct Query{
int l,r,id;
}q[N];
int len,n,m,k,a[N],ans[N],res,cnt[N];
int get(int x){
return x / len;
}
bool cmp(Query a,Query b){
if(get(a.l) != get(b.l)) return get(a.l) < get(b.l);
return a.r < b.r;
}
void Add(int v){ //这个东西可以结合 k=0 来理解
res += cnt[v ^ k];
cnt[v]++;
}
void Delet(int v){
cnt[v]--;
res -= cnt[v ^ k];
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%lld%lld%lld",&n,&m,&k);
len = sqrt(n);
for(int i=1; i<=n; i++){
int v;
scanf("%lld",&v); //维护前缀异或值
a[i] = a[i-1] ^ v;
}
for(int i=1; i<=m; i++){
scanf("%lld%lld",&q[i].l,&q[i].r);
q[i].id = i;q[i].l--;
}
sort(q+1,q+m+1,cmp);
int now_l = 1,now_r = 0;
for(int i=1; i<=m; i++){
int l = q[i].l,r = q[i].r;
while(now_l > l) Add(a[--now_l]);
while(now_r < r) Add(a[++now_r]);
while(now_l < l) Delet(a[now_l++]);
while(now_r > r) Delet(a[now_r--]);
ans[q[i].id] = res;
}
for(int i=1; i<=m; i++){
printf("%lld\n",ans[i]);
}
return 0;
}
总结:
对于此类题目,第一眼就应该想到维护前缀或者后缀,根据前后缀表达出题目要求的式子。
[AHOI2013]作业
题目分析:
我们会发现我们可以使用莫队做到获取一个区间的信息,但是获取某个区间大于等于 \(a\) 且小于等于 \(b\) 的数的信息就不可能简单的莫队就可以解决了。
难道使用线段树吗?
时间复杂度显然不对:\(O(n\sqrt{n}\log n)\)
我们考虑从莫队的时间复杂度开始分析:
莫队的时间复杂度是 \(O(n \sqrt{n})\) 修改,\(O(m)\) 查询。
也就是莫队的查询复杂度很低,而修改复杂度很高,为了平衡复杂度我们就可以考虑使用修改复杂度很低而查询复杂度很高的东西。
也就是我们的值域分块,可以做到 \(O(1)\) 修改,\(O(\sqrt{n})\) 查询。
也就是我们的总复杂度依旧是 \(O(n\sqrt{n})\)。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5+5;
struct Query{
int l,r,a,b,id;
}q[N];
int mx,len,ans1[N],ans2[N],a[N],bl[N],L[N],R[N],cnt[N],sum1[N],sum2[N]; //cnt 记录每个数,sum1 记录一个块内数的个数,sum2 记录一个块内数的种类
int get(int x){
return x / len;
}
bool cmp(Query a,Query b){
if(get(a.l) != get(b.l)) return get(a.l) < get(b.l);
return a.r < b.r;
}
void get_block(){
int sz = mx / len;
for(int i=1; i<=sz; i++){
L[i] = (i-1) * len + 1,R[i] = i * len;
}
R[sz] = mx;
for(int i=1; i<=sz; i++){
for(int j=L[i]; j<=R[i]; j++){
bl[j] = i;
}
}
}
void Add(int pos){
cnt[a[pos]]++;
sum1[bl[a[pos]]]++;
if(cnt[a[pos]] == 1) sum2[bl[a[pos]]]++;
}
void Delet(int pos){
cnt[a[pos]]--;
sum1[bl[a[pos]]]--;
if(cnt[a[pos]] == 0) sum2[bl[a[pos]]]--;
}
int get_sum(int l,int r){
if(l > mx) return 0;
r = min(r,mx);
int nl = bl[l] + 1;
int nr = bl[r] - 1;
int ans = 0;
if(bl[r] - bl[l] <= 1){
for(int i=l; i<=r; i++) ans += cnt[i];
return ans;
}
for(int i=nl; i<=nr; i++) ans += sum1[i];
for(int i=l; i<=R[bl[l]]; i++) ans += cnt[i];
for(int i=L[bl[r]]; i<=r; i++) ans += cnt[i];
return ans;
}
int get_num(int l,int r){
if(l > mx) return 0;
r = min(r,mx);
int nl = bl[l] + 1;
int nr = bl[r] - 1;
int ans = 0;
if(bl[r] - bl[l] <= 1){
for(int i=l; i<=r; i++) ans += cnt[i] >= 1;
return ans;
}
for(int i=nl; i<=nr; i++) ans += sum2[i];
for(int i=l; i<=R[bl[l]]; i++) ans += cnt[i] >= 1;
for(int i=L[bl[r]]; i<=r; i++) ans += cnt[i] >= 1;
return ans;
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n,m;
scanf("%d%d",&n,&m);
mx = 0;
for(int i=1;i <=n; i++){
scanf("%d",&a[i]);
mx = max(mx,a[i]);
}
len = sqrt(mx);
get_block();
for(int i=1; i<=m; i++){
scanf("%d%d%d%d",&q[i].l,&q[i].r,&q[i].a,&q[i].b);
q[i].id = i;
}
len = (1.0 * n / sqrt(m)) + 1;
sort(q+1,q+m+1,cmp);
int now_l=1,now_r = 0;
for(int i=1; i<=m; i++){
int l = q[i].l,r = q[i].r,a = q[i].a,b = q[i].b;
while(now_l > l) Add(--now_l);
while(now_r < r) Add(++now_r);
while(now_l < l) Delet(now_l++);
while(now_r > r) Delet(now_r--);
ans1[q[i].id] = get_sum(a,b);
ans2[q[i].id] = get_num(a,b);
}
for(int i=1; i<=m; i++){
printf("%d %d\n",ans1[i],ans2[i]);
}
return 0;
}
总结:
使用分块来平衡复杂度,也是一个常见的套路。
[SDOI2008] 郁闷的小 J
题目分析:
注意本题的放书方法:将某些位置上的书拿掉并换成新购的数。
也就是说我们的操作是修改一个数然后查询区间内某个数出现的个数。
显然可以离散化之后使用带修莫队求解。
代码:
点击查看代码
#include<bits/stdc++.h>
#define PII pair<int,int>
using namespace std;
const int N = 1e6+5;
struct Query{
int l,r,k,pos,id;
}query[N];
PII change[N];
int len,now,tot,now_l,now_r,change_sz,query_sz,b[N],ans[N],a[N],sum[N];
int get(int x){
return x / len;
}
bool cmp(Query a,Query b){
if(get(a.l) != get(b.l)) return get(a.l) < get(b.l);
if(get(a.r) != get(b.r)) return get(a.r) < get(b.r);
return a.pos < b.pos;
}
void Change(int i){
int pos = change[i].first;
int val = change[i].second;
if(pos >= now_l && pos <= now_r){
sum[a[pos]]--;sum[val]++;
}
swap(a[pos],change[i].second);
}
void Add(int pos){
sum[a[pos]]++;
}
void Delet(int pos){
sum[a[pos]]--;
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n,m;
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++) scanf("%d",&a[i]),b[++tot] = a[i];
for(int i=1; i<=m; i++){
char opt[2];
int l,r,k,pos,val;
scanf("%s",opt);
if(opt[0] == 'C'){
scanf("%d%d",&pos,&val);
change[++change_sz] = {pos,val};
b[++tot] = val;
}
else if(opt[0] == 'Q'){
scanf("%d%d%d",&l,&r,&k);
++query_sz;
query[query_sz].l = l;query[query_sz].r = r;query[query_sz].k = k;query[query_sz].pos = change_sz;query[query_sz].id = query_sz;
b[++tot] = k;
}
}
sort(b+1,b+tot+1);
tot = unique(b+1,b+tot+1) - b - 1;
for(int i=1; i<=change_sz; i++){
change[i].second = lower_bound(b+1,b+tot+1,change[i].second) - b;
}
for(int i=1; i<=query_sz; i++){
query[i].k = lower_bound(b+1,b+tot+1,query[i].k) - b;
}
for(int i=1; i<=n; i++){
a[i] = lower_bound(b+1,b+tot+1,a[i]) - b;
}
len = pow(n,2.0/3);
sort(query+1,query+query_sz+1,cmp);
now = 0,now_l = 1,now_r = 0;
for(int i=1; i<=query_sz; i++){
int l = query[i].l,r = query[i].r,ti = query[i].pos;
while(now_l > l) Add(--now_l);
while(now_r < r) Add(++now_r);
while(now_l < l) Delet(now_l++);
while(now_r > r) Delet(now_r--);
while(now < ti) Change(++now);
while(now > ti) Change(now--);
ans[query[i].id] = sum[query[i].k];
}
for(int i=1; i<=query_sz; i++){
printf("%d\n",ans[i]);
}
return 0;
}
[WC2013] 糖果公园
题目描述:
简化一下就是实现树上带修莫队,维护贡献就是根据题目里给出的式子在增加或删除的时候维护一下就好了。
其实与树上莫队相比就是多了一维时间,那么就是在考虑完树上莫队的部分后再考虑时间就好了。
这里判断某个修改是否有贡献就简单很多了,就直接判断这个位置有没有被计算入贡献就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define PII pair<int,int>
using namespace std;
const int N = 4e5+5;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N];
struct Query{
int id,pos,l,r,w,opt;
}query[N];
int n,m,q,tot,cnt,len,res,change_sz,query_sz,ans[N],tmp1[N],tmp2[N],head[N],dep[N],fa[N][24],euler[N],fir[N],lst[N],v[N],w[N],c[N];
PII change[N];
int get(int x){
return x / len;
}
bool cmp(Query a,Query b){
if(get(a.l) != get(b.l)) return get(a.l) < get(b.l);
if(get(a.r) != get(b.r)) return get(a.r) < get(b.r);
return a.pos < b.pos;
}
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void dfs(int now,int fath){
fir[now] = ++tot;dep[now] = dep[fath] + 1;fa[now][0] = fath;
euler[tot] = now;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to ==fath) continue;
dfs(to,now);
}
lst[now] = ++tot;
euler[tot] = now;
}
void pre_lca(){
for(int i=1; i<=20; i++){
for(int j=1; j<=n; j++){
fa[j][i] = fa[fa[j][i-1]][i-1];
}
}
}
int get_lca(int x,int y){
if(dep[x] < dep[y]) swap(x,y);
for(int i=20;i >=0; i--){
if(dep[fa[x][i]] >= dep[y]){
x = fa[x][i];
}
}
if(x == y) return x;
for(int i =20;i >=0; i--){
if(fa[x][i] != fa[y][i]){
x = fa[x][i];
y = fa[y][i];
}
}
return fa[x][0];
}
void Add(int pos){
pos = euler[pos];
tmp1[pos] ^= 1;
if(tmp1[pos] == 1){
tmp2[c[pos]]++;
res += v[c[pos]] * w[tmp2[c[pos]]];
}
else{
res -= v[c[pos]] * w[tmp2[c[pos]]];
tmp2[c[pos]]--;
}
}
void Change(int ti){ //考虑这一次询问
int pos = change[ti].first;
int val = change[ti].second;
if(tmp1[pos]){
res -= v[c[pos]] * w[tmp2[c[pos]]];
tmp2[c[pos]]--;
tmp2[val]++;
res += v[val] * w[tmp2[val]];
}
swap(c[pos],change[ti].second); //注意这里的交换
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%lld%lld%lld",&n,&m,&q);
for(int i=1; i<=m; i++) scanf("%lld",&v[i]);
for(int i=1; i<=n; i++) scanf("%lld",&w[i]);
for(int i=1; i<n; i++){
int from,to;
scanf("%lld%lld",&from,&to);
add_edge(from,to);add_edge(to,from);
}
for(int i=1; i<=n; i++) scanf("%lld",&c[i]);
dfs(1,0);
pre_lca();
for(int i=1; i<=q; i++){
int type,x,y;
scanf("%lld%lld%lld",&type,&x,&y);
if(type == 0){
change[++change_sz] = {x,y};
}
else if(type == 1){
if(fir[x] > fir[y]) swap(x,y);
int lca = get_lca(x,y);
++query_sz;
if(lca == x) query[query_sz].l = fir[x],query[query_sz].r = fir[y];
else query[query_sz].l = lst[x],query[query_sz].r = fir[y],query[query_sz].opt = 1,query[query_sz].w = fir[lca];
query[query_sz].id = query_sz,query[query_sz].pos = change_sz;
}
}
len = sqrt(tot);
sort(query+1,query+query_sz+1,cmp);
int now = 0,now_l = 1,now_r = 0;
for(int i=1; i<=query_sz; i++){
int l = query[i].l,r = query[i].r,ti = query[i].pos,pos = query[i].id;
while(now_l > l) Add(--now_l);
while(now_r < r) Add(++now_r);
while(now_l < l) Add(now_l++);
while(now_r > r) Add(now_r--);
if(query[i].opt == 1) Add(query[i].w);
while(now < ti) Change(++now);
while(now > ti) Change(now--);
ans[pos] = res;
if(query[i].opt == 1) Add(query[i].w);
}
for(int i=1; i<=query_sz; i++){
printf("%lld\n",ans[i]);
}
return 0;
}
9.15
CF1622C Set or Decrease
题目分析:
显然可以二分操作次数。
按照贪心的思路,我们肯定是将最小的值先变小,再将较大的值直接赋值为最小值。
那么二分完了再判断一下最小值减多少次就好了。
代码:
点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
int t, n, k, a[200005], sum[200005];
bool check(int mid){
int minn = 1e18;
for (int i = max(0ll, mid - n + 1); i <= mid; i++){
int j = mid - i;
minn = min(minn, sum[n] - ((sum[n] - sum[n - j]) - j * (a[1] - i)) - i);
}
return minn <= k;
}
signed main(){
cin >> t;
while (t--){
cin >> n >> k;
for (int i = 0; i <= n + 3; i++){
sum[i] = 0;
}
for (int i = 1; i <= n; i++){
cin >> a[i];
}
sort(a + 1, a + n + 1);
for (int i = 1; i <= n; i++){
sum[i] = sum[i - 1] + a[i];
}
if (sum[n] <= k){
cout << 0 << endl;
continue;
}
int l = 0, r = sum[n] - k, ans;
while (l <= r){
int mid = (l + r) / 2;
if (check(mid)){
r = mid - 1;
ans = mid;
}
else{
l = mid + 1;
}
}
cout << ans << endl;
}
return 0;
}
CF1622D Shuffle
题目分析:
我们可以发现,我们每次都一定是更改一个极长的符合条件的字符串。
我们可以发现对于一个长度为 \(len\) 的字符串,假设其中有 \(cnt\) 个 \(1\),那么它的所有排列方式就是 \(\binom{len}{cnt}\)。
可是这样会有重复的情况。
但是我们会发现重复的情况只有下面这一种:
对于连续的两个极长子串 \([l_1,r_1]\) 和 \([l_2,r_2]\) 满足,\(l_1 < l_2 < r_1 < r_2\) 且 \([l_1,l_2),(r_1,r_2]\) 均不变的情况下的方案数也就是重复的方案数,那么也就是 \([l_2,r_1]\) 的所有排列的方案数,需要注意不变这种情况,所以需要加一减一来维护一下。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define PII pair<int,int>
using namespace std;
const int MOD = 998244353;
const int N = 5e3+5;
int pre[N],inv[N];
char s[N];
vector<PII> v;
int mod(int x){
return ((x % MOD)+MOD)%MOD;
}
int power(int a,int b){
int res = 1;
while(b){
if(b & 1) res = mod(res * a);
a = mod(a * a);
b>>=1;
}
return res;
}
int calc(int a,int b){
return mod(pre[a] * mod(inv[b] * inv[a - b]));
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n,k;
scanf("%lld%lld",&n,&k);
for(int i=1; i<=n; i++) scanf("%s",s+1);
pre[0] = inv[0] = 1;
for(int i=1; i<=n; i++) pre[i] = mod(pre[i-1] * i);
inv[n] = power(pre[n],MOD-2);
for(int i=n-1; i>=1; i--) inv[i] = mod(inv[i+1] * (i + 1));
int ans = 0;
for(int i=1; i<=n; i++){
if(s[i-1] == '0') continue; //不是极长的
int tmp = 0,j = i;
for(j=i; j<=n; j++){
if(s[j] == '1' && tmp == k) break;
if(s[j] == '1') tmp++;
}
if(s[j] == '1' || j > n) j--;
if(tmp != k || i > j) continue;
ans = mod(ans + calc(j-i+1,tmp) - 1);
v.push_back({i,j});
if(j == n) break;
}
for(int i=1; i<v.size(); i++){
int l = v[i].first;
int r = v[i-1].second;
if(l > r) continue;
int tmp = 0;
for(int j=l; j<=r; j++) tmp += (s[j] == '1');
ans = mod(ans - calc(r-l+1,tmp) + 1);
}
printf("%lld",mod(ans+1));
return 0;
}
CF1622E Math Test
题目分析:
我们可以发现人的数量很少,所以我们就可以考虑枚举每一个人的 \(|r_i - s_i|\) 取正或者取负。
然后我们就可以愉快的化式子了:
我们在这里设 \(a_1,a_2,\cdots,a_m\) 为 \(m\) 道题的分数,是我们要去分配的
我们令 \(d_j = \sum_{i=1}^n c_ip_{i,j}\)
我们要使得 \(Ans\) 最大也就是使得 \(\sum_{i=1}^n a_id_i\) 最小,根据排序不等式,我们就是要让尽可能小的 \(d_i\) 分配到尽可能大的 \(a_i\)
我们在枚举的过程中可能出现不合法的情况,但是不合法的情况肯定不如我们的合法的最优解优。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define PII pair<int,int>
using namespace std;
const int N = 1e4+5;
const int INF = 1e6+5;
int res[N],x[N],c[N],ans;
char s[20][N];
PII d[N];
bool cmp(PII a,PII b){
return a.first < b.first;
}
signed main(){
int t;
scanf("%lld",&t);
while(t--){
ans = -INF;
int n,m;
scanf("%lld%lld",&n,&m);
for(int i=1; i<=n; i++) scanf("%lld",&x[i]);
for(int i=1; i<=n; i++) scanf("%s",s[i] + 1);
for(int S=0; S<=(1<<n)-1; S++){ //直接暴力枚举
for(int i=1; i<=n; i++){
if(S & (1<<(i-1))) c[i] = 1;
else c[i] = -1;
}
for(int i=1; i<=m; i++){
d[i].first = 0;
d[i].second = i;
for(int j=1; j<=n; j++){
if(s[j][i] == '0') continue;
d[i].first += c[j];
}
}
sort(d+1,d+m+1,cmp);
int tmp = 0;
for(int i=1; i<=n; i++) tmp += c[i] * x[i];
for(int i=1; i<=m; i++) tmp -= d[i].first * (m - i + 1); //排序不等式
if(tmp > ans){
ans = tmp;
for(int i=1; i<=m; i++){
res[d[i].second] = (m - i + 1);
}
}
}
for(int i=1; i<=m; i++) printf("%lld ",res[i]);
printf("\n");
}
return 0;
}
CF1622F Quadratic Set
题目分析:
我们会发现答案一定不会小于 \(n-3\)。
因为我们如果将 \(\{2,\dfrac{n-1}{2},n\}\) 去掉,一定是一个平方数。
那么其实判断就容易很多了。
我们设 \(all = \prod_{i=1}^n f[i]\)
那么我们其实就是判断下面三个东西是否成立:
- \(all\) 是否是完全平方数
- 是否存在 \(x\),使得 \(\dfrac{all}{x!}\) 是完全平方数
- 是否存在 \(x \not= y\),使得 \(\dfrac{all}{x!y!}\) 是完全平方数
我们会发现是否是平方数其实就是看其质因子的奇偶,所以就考虑对于每一个质数赋一个 \(\text{hash}\) 值,到时候判断异或值是否为 \(0\) 就能判断是否为平方数。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define uid(i,j,k) uniform_int_distribution<long long> i(j,k)
#define Rand(s) s(rand_num)
using namespace std;
unsigned seed=chrono::system_clock::now().time_since_epoch().count();
mt19937 rand_num(seed);
uid(R,0,0xffffffffffffffff);
const int N = 2e6+5;
int pri[N],H[N],mn[N],f[N],cnt;
map<int,int> mp;
bool vis[N],flag[N];
void pre_work(int mx){
for(int i=1; i<=mx; i++) mn[i] = i;
for(int i=2; i<=mx; i++){
if(vis[i]) continue;
H[i] = Rand(R);
if(i > 10000) continue;
for(int j = i*i; j<=mx; j+=i){
vis[j] = true;
mn[j] = min(mn[j],i);
}
}
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n;
scanf("%lld",&n);
pre_work(n);
// for(int i=1; i<=n; i++){
// printf("%lld\n",mn[i]);
// }
int now;
for(int i=2; i<=n; i++){
f[i] = f[i-1];
now = i;
while(now > 1){
f[i] ^= H[mn[now]];
now /= mn[now];
}
mp[f[i]] = i;
}
int all = 0;
for(int i=1; i<=n; i++) all ^= f[i];
int res = 0;
if(all != 0){
if(mp[all] > 0){
flag[mp[all]] = true;
res=1;
}
else{
bool tmp = false;
for(int i=1; i<=n; i++){
if(mp[all ^ f[i]] > 0){
flag[mp[all ^ f[i]]] = true;
flag[i] = true;
// printf("%lld %lld\n",mp[all ^ f[i]],i);
tmp = true;
res = 2;
break;
}
}
if(!tmp){
flag[2] = true;
flag[(n-1)>>1] = true;
flag[n] = true;
res = 3;
}
}
}
printf("%lld\n",n - res);
for(int i=1; i<=n; i++){
if(!flag[i])
printf("%lld ",i);
}
return 0;
}
POJ3463 Sightseeing
题目分析:
看到最短路的长度减一显然可以想到次短路。
所以这个题其实就是让我们维护一个最短路、次短路以及他们的路径条数。
那么就正常维护就好了。
代码:
点击查看代码
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<queue>
#define int long long
#define PII pair<int,int>
using namespace std;
const int N = 2e4+5;
const int INF = 0x3f3f3f3f;
struct edge{
int nxt,to,val;
edge(){}
edge(int _nxt,int _to,int _val){
nxt = _nxt,to = _to,val = _val;
}
}e[N];
struct node{
int val,now,opt;
node(){}
node(int _val,int _now,int _opt){
val = _val,now = _now,opt = _opt;
}
};
int cnt,s,t,n,m,head[N],dis[N][2],tot[N][2];
bool vis[N][2];
bool operator < (node a,node b){
return a.val > b.val;
}
void add_edge(int from,int to,int val){
e[++cnt] = edge(head[from],to,val);
head[from] = cnt;
}
void dij(){
memset(dis,INF,sizeof(dis));memset(tot,0,sizeof(tot));memset(vis,false,sizeof(vis));
priority_queue<node> q;
dis[s][0] = 0;tot[s][0] = 1;q.push(node(dis[s][0],s,0));
while(!q.empty()){
int now = q.top().now;
int opt = q.top().opt;q.pop();
if(vis[now][opt]) continue;
vis[now][opt] = true;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(dis[to][0] > dis[now][opt] + e[i].val){
dis[to][1] = dis[to][0];tot[to][1] = tot[to][0];
if(dis[to][0] != INF) q.push(node(dis[to][1],to,1));
dis[to][0] = dis[now][opt] + e[i].val;
tot[to][0] = tot[now][opt];
q.push(node(dis[to][0],to,0));
}
else if(dis[to][0] == dis[now][opt] + e[i].val){
tot[to][0] += tot[now][opt];
}
else if(dis[to][1] > dis[now][opt] + e[i].val){
dis[to][1] = dis[now][opt] + e[i].val;
tot[to][1] = tot[now][opt];
q.push(node(dis[to][1],to,1));
}
else if(dis[to][1] == dis[now][opt] + e[i].val){
tot[to][1] += tot[now][opt];
}
}
}
}
signed main(){
int ti;
scanf("%lld",&ti);
while(ti--){
memset(head,0,sizeof(head));cnt=0;
scanf("%lld%lld",&n,&m);
for(int i=1; i<=m; i++){
int from,to,val;
scanf("%lld%lld%lld",&from,&to,&val);
add_edge(from,to,val);
}
scanf("%lld%lld",&s,&t);
dij();
if(dis[t][0] == dis[t][1] - 1)
printf("%lld\n",tot[t][0] + tot[t][1]);
else
printf("%lld\n",tot[t][0]);
}
return 0;
}
HDU4424 Conquer a New Region
题目分析:
我们可以先将边从大到小排序,然后依次加入。
这样就可以保证我们每次加入的一定是最小的限制的那条边。
然后使用并查集维护联通性以及答案就结束了。
可以发现这样的贪心一定可以使得我们的尽可能小的边尽可能大也就是答案尽可能大。
代码:
点击查看代码
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define int long long
using namespace std;
const int N = 4e5+5;
struct edge{
int from,to,val;
}a[N];
int fa[N],sz[N],ans[N];
bool cmp(edge a,edge b){
return a.val > b.val;
}
int find(int x){
if(fa[x] == x) return x;
return fa[x] = find(fa[x]);
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n;
while(~scanf("%lld",&n)){
for(int i=1; i<n; i++){
scanf("%lld%lld%lld",&a[i].from,&a[i].to,&a[i].val);
}
sort(a+1,a+n,cmp);
for(int i=1; i<=n; i++) fa[i] = i,sz[i] = 1,ans[i] = 0;
for(int i=1; i<n; i++){
int x = find(a[i].from);
int y = find(a[i].to);
if(x != y){
int ans1 = ans[x] + a[i].val * sz[y];
int ans2 = ans[y] + a[i].val * sz[x];
if(ans1 > ans2){
fa[y] = x;
sz[x] += sz[y];
ans[x] = ans1;
}
else{
fa[x] = y;
sz[y] += sz[x];
ans[y] = ans2;
}
}
}
printf("%lld\n",ans[find(1)]);
}
return 0;
}
POJ1734 Sightseeing trip
题目分析:
题意就是让我们求一个最小环,并且输出路径长度。
我们如果使用正常做法:断边、求最短路、更新答案。会 \(T\) 的很惨。
但我们看到 \(n\) 很小更换一下做法,使用 \(floyd\)。
即我们将 \(k\) 节点当作我们环上编号最大的一个节点,在不用 \(k\) 更新最短路的时候,求出 \(d[i][k] + d[k][j] + w[i][j]\) 的最小值,也就是这种环的权值的最小值。
用这个更新我们的答案就好了。
代码:
点击查看代码
#include<cstdio>
#include<cmath>
#include<algorithm>
#define int long long
using namespace std;
const int N = 2e5+5;
const int INF = 1e9+5;
int ans = INF,tot,path[N],n,m;
int d[200][200],w[200][200],pre[200][200];
//pre[i][j] 实际上记录的是 i 到 j 的最短路的最后一条边的上一个点
void Floyed(){
for(int k=1; k<=n; k++){
for(int i=1; i<k; i++){
for(int j=i+1; j<k; j++){
if(ans > d[i][k] + d[k][j] + w[i][j]){
ans = d[i][k] + d[k][j] + w[i][j];
int now = j;tot = 0;
while(i != now){
path[++tot] = now;
now = pre[i][now];
}
path[++tot] = i;path[++tot] = k;
}
}
}
for(int i=1; i<=n; i++){
for(int j=1; j<=n; j++){
if(w[i][j] > w[i][k] + w[k][j]){
w[i][j] = w[i][k] + w[k][j];
pre[i][j] = pre[k][j];
}
}
}
}
}
void pre_work(){
for(int i=0; i<=n; i++){
for(int j=0; j<=n; j++){
w[i][j] = d[i][j] = INF;
pre[i][j] = i;
}
}
}
signed main(){
while(~scanf("%lld%lld",&n,&m)){
pre_work();
for(int i=1; i<=m; i++){
int from,to,val;
scanf("%lld%lld%lld",&from,&to,&val);
w[from][to] = w[to][from] = min(w[from][to],val);
d[from][to] = d[to][from] = w[from][to];
}
Floyed();
if(ans == INF) printf("No solution.\n");
else{
for(int i=1; i<=tot; i++){
printf("%lld ",path[i]);
}
printf("\n");
}
}
return 0;
}
9.16
CF686D Kay and Snowflake
题目分析:
这个题就是让我们求解以每个点为根的子树的重心。
我们可以发现一个性质:重心一定不会下移,一定在某个子树的重心到他的路径上。
我们将重心删除之后一定存在每一棵子树的大小都小于等于原树的一半,所以我们只需要取重儿子的重心上移就好了。
因为如果选择其他子树,大概率包含重儿子的这个子树会大于原树的一半。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 6e5+5;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N];
int cnt,head[N],sz[N],mx[N],ans[N],fa[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
bool check(int now,int size){
if(sz[mx[now]] * 2 <= size && (size - sz[now]) * 2 <= size)
return true;
return false;
}
void dfs(int now,int fath){
mx[now] = 0;
sz[now] = 1;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
dfs(to,now);
sz[now] += sz[to];
if(sz[to] > sz[mx[now]]) mx[now] = to;
}
if(sz[now] == 1) ans[now] = now;
else{
ans[now] = ans[mx[now]];
while(!check(ans[now],sz[now])){
ans[now] = fa[ans[now]];
}
}
}
int main(){
int n,q;
scanf("%d%d",&n,&q);
for(int i=2; i<=n; i++){
scanf("%d",&fa[i]);
add_edge(fa[i],i);
}
dfs(1,0);
for(int i=1; i<=q; i++){
int v;
scanf("%d",&v);
printf("%d\n",ans[v]);
}
return 0;
}
CF842C Ilya And The Tree
题目分析:
我们只会选择一个节点删除,而当我们删除那个节点确定了之后所有的值就确定了。
所以就可以考虑记忆化搜索,直接枚举当前点是否删除然后向下搜索就好了。
因为我们的 gcd 数量必然不会很多,所以能跑的很快。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 4e5+5;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N];
int cnt,head[N],f[N],a[N];
map<int,bool> vis[N][3];
int gcd(int a,int b){
if(b == 0) return a;
return gcd(b,a%b);
}
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void dfs(int now,int fath,bool flag,int ans){
if(vis[now][flag][ans]) return;
vis[now][flag][ans] = true;
int tmp = gcd(ans,a[now]);
f[now] = max(f[now],tmp);
if(flag == 0) f[now] = max(f[now],ans);
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
dfs(to,now,flag,tmp);
if(flag == 0) dfs(to,now,1,ans);
}
}
int main(){
int n;
scanf("%d",&n);
for(int i=1; i<=n; i++) scanf("%d",&a[i]);
for(int i=1; i<n; i++){
int from,to;
scanf("%d%d",&from,&to);
add_edge(from,to);add_edge(to,from);
}
dfs(1,0,0,0);
for(int i=1; i<=n; i++){
printf("%d ",f[i]);
}
return 0;
}
[NOI2014]魔法森林
题目分析:
我们可以考虑按 \(a\) 从小到大排序,并且维护 \(b\) 的最小生成树,也就是使得 \(1\) 到 \(n\) 上 \(b\) 的最大值最小。
这样就只需要查询 \(1\) 到 \(n\) 的 \(b\) 的最大值再加上当前的 \(a\) 用来更新我们的答案就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
const int INF = 1e9+5;
struct cyx {int a, b, x, y;} q[N];
bool comp(cyx a, cyx b) {return a.a < b.a;}
int n,m,fa[N],f[N],ch[N][2],val[N],tag[N],pos[N],que[N];
int find(int x){
if(f[x] == x) return x;
return f[x] = find(f[x]);
}
void merge(int x,int y){
if(find(x) == find(y)) return;
f[find(x)] = find(y);
}
bool getson(int x){
return ch[fa[x]][1] == x;
}
bool isroot(int x){
return ch[fa[x]][getson(x)] != x;
}
void reverse(int x){
swap(ch[x][0],ch[x][1]);
tag[x] ^= 1;
}
void pushdown(int x){
if(tag[x]){
if(ch[x][0]) reverse(ch[x][0]);
if(ch[x][1]) reverse(ch[x][1]);
tag[x] ^= 1;
}
}
void pushup(int x){
pos[x] = x;
if(ch[x][0] && val[pos[ch[x][0]]] > val[pos[x]]) pos[x] = pos[ch[x][0]];
if(ch[x][1] && val[pos[ch[x][1]]] > val[pos[x]]) pos[x] = pos[ch[x][1]];
}
void rotate(int x){
int y = fa[x],z = fa[y],k = getson(x);
if(!isroot(y)) ch[z][getson(y)] = x;fa[x] = z; //认父不认子
ch[y][k] = ch[x][k^1];if(ch[x][k^1]) fa[ch[x][k^1]] = y;
ch[x][k^1] = y;fa[y] = x;
pushup(y);pushup(x);
}
void splay(int x){
int l = 0;
que[++l] = x; //因为我们在 splay 的时候需要考虑左右儿子的顺序,所以需要下放标记
for(int y = x;!isroot(y);y=fa[y]) que[++l] = fa[y];
for(int i=l; i; i--) pushdown(que[i]);
for(;!isroot(x);rotate(x))
if(!isroot(fa[x])) rotate(getson(x) == getson(fa[x]) ? fa[x] : x);
}
void access(int x){
for(int pre=0;x;pre=x,x=fa[x]){
splay(x);ch[x][1] = pre;
pushup(x);
}
}
void makeroot(int x){
access(x);
splay(x);
reverse(x);
}
int findroot(int x){
access(x);splay(x);
while(ch[x][0]) x = ch[x][0];
return x;
}
void link(int x,int y){
// printf("link %d %d\n",x,y);
makeroot(x);
fa[x] = y;
}
void cut(int x,int y){
// printf("cut %d %d\n",x,y);
makeroot(x);access(y);splay(y);
if(fa[x] == y && !ch[x][1]){
ch[y][0] = fa[x] = 0;
}
pushup(y);
}
int split(int x,int y){
makeroot(x);access(y);splay(y);
return pos[y];
}
void add_edge(int u,int v,int h,int id){
bool p = false;
if(find(u) == find(v)){
split(u,v);
int k = pos[v];
if(val[k] > h){
cut(q[k-n].x,k);
cut(k,q[k-n].y);
}
else p = true;
}
else merge(u,v);
if(!p){
link(u,id+n);
link(id+n,v);
}
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1; i<=m; i++){
scanf("%d%d%d%d",&q[i].x,&q[i].y,&q[i].a,&q[i].b);
}
sort(q+1,q+m+1,comp);
for(int i=1; i<=n+m; i++) f[i] = pos[i] = i;
for(int i=n+1; i<=n+m; i++) val[i] = q[i - n].b;
int ans = INF;
for(int i=1; i<=m; i++){
add_edge(q[i].x,q[i].y,q[i].b,i);
if(find(1) == find(n)){
ans = min(ans,q[i].a + val[split(1,n)]);
}
}
if(ans < INF) printf("%d\n",ans);
else printf("-1");
return 0;
}
总结:
边权转点权的常用套路:对于边额外建一个点
[TJOI2015]旅游
题目分析:
注意本题不能简单的维护一个最大值和一个最小值。
因为从 \(a\) 到 \(b\) 先买的才能在以后卖掉。
所以我们需要维护 \(mxl[i]\) 和 \(mxr[i]\) 表示从 \(i\) 向左走/向右走的最大利润。
我们维护 \(mxr\) 主要是因为当我们翻转的时候 \(mxl\) 就不好表示了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
const int INF = 1e9+5;
int top,st[N],fa[N],ch[N][2],add[N],val[N],tag[N],mx[N],mn[N],mxl[N],mxr[N];
int getson(int x){
return ch[fa[x]][1] == x;
}
int isroot(int x){
return ch[fa[x]][getson(x)] != x;
}
void Add(int p,int w){
add[p] += w;val[p] += w;
mx[p] += w;mn[p] += w;
}
void pushup(int x){
mx[x] = max(val[x],max(mx[ch[x][0]],mx[ch[x][1]]));
mn[x] = min(val[x],min(mn[ch[x][0]],mn[ch[x][1]]));
mxl[x] = max(max(mxl[ch[x][0]],mxl[ch[x][1]]),max(mx[ch[x][0]],val[x]) - min(mn[ch[x][1]],val[x]));
mxr[x] = max(max(mxr[ch[x][0]],mxr[ch[x][1]]),max(mx[ch[x][1]],val[x]) - min(mn[ch[x][0]],val[x]));
}
void reverse(int x){
swap(mxl[x],mxr[x]);
swap(ch[x][0],ch[x][1]);
tag[x] ^= 1;
}
void pushdown(int x){
if(tag[x]){
if(ch[x][0]) reverse(ch[x][0]);
if(ch[x][1]) reverse(ch[x][1]);
tag[x] ^= 1;
}
if(add[x]){
if(ch[x][0]) Add(ch[x][0],add[x]);
if(ch[x][1]) Add(ch[x][1],add[x]);
add[x] = 0;
}
}
void rotate(int x){
int y = fa[x],z = fa[y],k = getson(x);
if(!isroot(y)) ch[z][getson(y)] = x;fa[x] = z;
ch[y][k] = ch[x][k^1];if(ch[x][k^1]) fa[ch[x][k^1]] = y;
ch[x][k^1] = y;fa[y] = x;
pushup(y);pushup(x);
}
void splay(int x){
top = 1;st[1] = x;
for(int y = x;!isroot(y); y = fa[y]) st[++top] = fa[y];
for(int i=top; i; i--) pushdown(st[i]);
for(;!isroot(x);rotate(x))
if(!isroot(fa[x])) rotate(getson(x) == getson(fa[x]) ? fa[x] : x);
}
void access(int x){
for(int pre=0;x;pre=x,x=fa[x]){
splay(x);ch[x][1] = pre;
pushup(x);
}
}
void makeroot(int x){
access(x);splay(x);
reverse(x);
}
void link(int x,int y){
makeroot(x);
fa[x] = y;
}
int split(int x,int y){
makeroot(y);access(x);splay(x);
return x;
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
mn[0] = INF;mx[0] = -INF;
int n;
scanf("%d",&n);
for(int i=1; i<=n; i++) scanf("%d",&val[i]);
for(int i=1; i<n; i++){
int from,to;
scanf("%d%d",&from,&to);
link(from,to);
}
int q;
scanf("%d",&q);
for(int i=1; i<=q; i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
split(u,v);
printf("%d\n",mxl[u]);
Add(u,w);
}
}
[WC2006]水管局长
题目分析:
看到题目:我们难道要维护一个支持删边求最小生成树吗?
显然不是。
将所有的操作逆序之后,就是动态加边求最小生成树了。
因为我们相当于求 \(u,v\) 路径上的最大值最小也就是求一个最小瓶颈生成树,最小生成树一定是最小瓶颈生成树。
注意因为我们将所有的操作逆序,所以输出答案就需要再逆回来。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e6+5;
struct Query{
int opt,from,to;
}que[N];
int top,n,m,q,ch[N][2],fa[N],f[N],tag[N],val[N],pos[N],st[N],from[N],to[N],w[N];
bool flag[N];
map<int,int> mp[N];
int find(int x){
if(f[x] == x) return x;
return f[x] = find(f[x]);
}
void merge(int x,int y){
if(find(x) == find(y)) return;
f[find(x)] = find(y);
}
bool getson(int x){
return ch[fa[x]][1] == x;
}
bool isroot(int x){
return ch[fa[x]][getson(x)] != x;
}
void reverse(int x){
swap(ch[x][0],ch[x][1]);
tag[x] ^= 1;
}
void pushdown(int x){
if(tag[x]){
if(ch[x][0]) reverse(ch[x][0]);
if(ch[x][1]) reverse(ch[x][1]);
tag[x] ^= 1;
}
}
void pushup(int x){
pos[x] = x;
if(ch[x][0] && val[pos[ch[x][0]]] > val[pos[x]]) pos[x] = pos[ch[x][0]];
if(ch[x][1] && val[pos[ch[x][1]]] > val[pos[x]]) pos[x] = pos[ch[x][1]];
}
void rotate(int x){
int y = fa[x],z = fa[y],k = getson(x);
if(!isroot(y)) ch[z][getson(y)] = x;fa[x] = z;
ch[y][k] = ch[x][k^1];if(ch[x][k^1]) fa[ch[x][k^1]] = y;
ch[x][k^1] = y;fa[y] = x;
pushup(y);pushup(x);
}
void splay(int x){
top=1,st[1] = x;
for(int y = x;!isroot(y);y=fa[y]) st[++top] = fa[y];
for(int i=top; i; i--) pushdown(st[i]); //犯错 *1
for(;!isroot(x);rotate(x))
if(!isroot(fa[x])) rotate(getson(fa[x]) == getson(x) ? fa[x] : x);
}
void access(int x){
for(int pre=0;x;pre=x,x=fa[x]){
splay(x);ch[x][1] = pre;
pushup(x);
}
}
void makeroot(int x){
access(x);splay(x);
reverse(x);
}
void link(int x,int y){
makeroot(x);
fa[x] = y;
}
void cut(int x,int y){
makeroot(x);access(y);splay(y);
if(fa[x] == y && !ch[x][1]){
ch[y][0] = fa[x] = 0;
}
pushup(y);
}
void split(int x,int y){
makeroot(x);access(y);splay(y);
}
void add_edge(int u,int v,int h){
bool p = false;
if(find(u) == find(v)){
split(u,v);
int k = pos[v];
if(val[k] > h){
cut(from[k-n],k);
cut(k,to[k-n]);
}
else p = true;
}
else merge(u,v);
if(!p){
link(u,mp[u][v]+n);
link(mp[u][v]+n,v);
}
}
signed main(){
scanf("%lld%lld%lld",&n,&m,&q);
for(int i=1; i<=m; i++){
scanf("%lld%lld%lld",&from[i],&to[i],&w[i]);
mp[from[i]][to[i]] = i;
mp[to[i]][from[i]] = i;
}
for(int i=1; i<=n+m; i++) f[i] = pos[i] = i;
for(int i=n+1; i<=n+m; i++) val[i] = w[i-n]; //val[i] 指的是 i-n 这条边的权值
for(int i=1; i<=q; i++){
scanf("%lld%lld%lld",&que[i].opt,&que[i].from,&que[i].to);
if(que[i].opt == 2){
flag[mp[que[i].from][que[i].to]] = true;
}
}
for(int i=1; i<=m; i++){
if(!flag[i]){
add_edge(from[i],to[i],w[i]);
}
}
stack<int> ans;
for(int i=q; i>=1; i--){
if(que[i].opt == 2){
add_edge(que[i].from,que[i].to,w[mp[que[i].from][que[i].to]]);
}
else{
split(que[i].from,que[i].to);
ans.push(val[pos[que[i].to]]);
}
}
while(!ans.empty()){
printf("%d\n",ans.top());
ans.pop();
}
return 0;
}
9.17
CF920C Swap Adjacent Elements
题目分析:
我们可以将每个 \(1\) 构成的可以交换的一段放在一起处理。
这样就将数组划分为了若干段,只要满足在段内可以使得排序后的条件满足就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 4e5+5;
int flag[N],a[N],b[N],tot;
char s[N];
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n;
scanf("%d",&n);
for(int i=1; i<=n; i++) scanf("%d",&a[i]);
scanf("%s",s+1);
for(int i=1; i<=n; i++){
if(s[i] == '1'){
if(flag[i] == 0) flag[i] = ++tot;
flag[i+1] = flag[i];
}
}
bool tag = true;
for(int i=1; i<=n; i++){
if(!flag[i] && a[i] != i) tag = false;
if(flag[i]){
int j = i;
int cnt = 0;
while(flag[j+1] == flag[i] && j+1 <= n) j++;
for(int k=i; k<=j; k++) b[++cnt] = a[k];
sort(b+1,b+cnt+1);
for(int k=i; k<=j; k++){
if(b[k - i + 1] != k) tag = false;
}
i = j;
}
}
if(tag) printf("YES\n");
else printf("NO\n");
return 0;
}
CF920F SUM and REPLACE
题目分析:
我们知道数 \(x\) 的约数个数为 \(O(\sqrt{n})\) 个,所以用一个数的约数个数来更换这个数就大概相当于给他开根号,所以我们最多四五次就可以不用操作了。
对于区间修改我们就记录区间最大值,当最大值小于等于 \(2\),那么显然就不用管了,否则就暴力向下递归。
对于 \(d\) 数组的预处理,我们可以考虑使用埃氏筛,复杂度 \(O(n \log n)\)
所以其实总复杂度完全可以过。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6+5;
int a[N],d[N],mx[N * 4],sum[N * 4];
void pre_work(int mx){
for(int i=1; i<=mx; i++)
for(int j=i; j<=mx; j+=i)
d[j]++;
}
void pushup(int now){
mx[now] = max(mx[now<<1],mx[now<<1|1]);
sum[now] = sum[now<<1] + sum[now<<1|1];
}
void build(int now,int now_l,int now_r){
if(now_l == now_r){
sum[now] = a[now_l];
mx[now] = a[now_l];
return;
}
int mid = (now_l + now_r)>>1;
build(now<<1,now_l,mid);
build(now<<1|1,mid+1,now_r);
pushup(now);
}
void change(int now,int now_l,int now_r,int l,int r){
if(mx[now] <= 2) return;
if(now_l == now_r){
mx[now] = d[mx[now]];
sum[now] = mx[now];
return;
}
int mid = (now_l + now_r)>>1;
if(l <= mid) change(now<<1,now_l,mid,l,r);
if(r > mid) change(now<<1|1,mid+1,now_r,l,r);
pushup(now);
}
int query(int now,int now_l,int now_r,int l,int r){
if(l <= now_l && r >= now_r) return sum[now];
int mid = (now_l + now_r)>>1;
int ans = 0;
if(l <= mid) ans += query(now<<1,now_l,mid,l,r);
if(r > mid) ans += query(now<<1|1,mid+1,now_r,l,r);
return ans;
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
pre_work(1000000);
int n,m;
scanf("%lld%lld",&n,&m);
for(int i=1; i<=n; i++) scanf("%lld",&a[i]);
build(1,1,n);
for(int i=1; i<=m; i++){
int opt,l,r;
scanf("%lld%lld%lld",&opt,&l,&r);
if(opt == 1) change(1,1,n,l,r);
else if(opt == 2) printf("%lld\n",query(1,1,n,l,r));
}
return 0;
}
9.19
[BJOI2014]大融合
题目分析:
这个题其实就是要维护子树大小。
我们可以考虑对于实边正常维护,对于虚边我们就放到其对应的父亲上。
因为我们的 LCT 认父不认子。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e6+5;
int tag[N],ch[N][2],sz[N],sz2[N],fa[N],st[N];
void reverse(int x){
tag[x] ^= 1;
swap(ch[x][0],ch[x][1]);
}
void pushup(int x){
if(x) sz[x] = sz[ch[x][0]] + sz[ch[x][1]] + 1 + sz2[x];
}
void pushdown(int x){
if(tag[x]){
if(ch[x][0]) reverse(ch[x][0]);
if(ch[x][1]) reverse(ch[x][1]);
tag[x] ^= 1;
}
}
bool getson(int x){
return ch[fa[x]][1] == x;
}
bool isroot(int x){
return ch[fa[x]][getson(x)] != x;
}
void rotate(int x){
int y = fa[x],z = fa[y],k = getson(x);
if(!isroot(y)) ch[z][getson(y)] = x;fa[x] = z;
ch[y][k] = ch[x][k^1]; if(ch[x][k^1]) fa[ch[x][k^1]] = y;
ch[x][k^1] = y;fa[y] = x;
pushup(y);pushup(x);pushup(z);
}
void splay(int x){
int top = 1;
st[1] = x;
for(int y = x;!isroot(y);y=fa[y]) st[++top] = fa[y];
for(int i = top; i; i--) pushdown(st[i]);
for(;!isroot(x);rotate(x))
if(!isroot(fa[x])) rotate(getson(x) == getson(fa[x]) ? fa[x] : x);
}
void access(int x){
for(int pre = 0;x;pre=x,x=fa[x]){
splay(x);
sz2[x] += sz[ch[x][1]] - sz[pre];
ch[x][1] = pre;pushup(x);
}
}
void makeroot(int x){
access(x);splay(x);
reverse(x);
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n,q;
scanf("%lld%lld",&n,&q);
for(int i=1; i<=q; i++){
char opt[2];
int x,y;
scanf("%s%lld%lld",opt,&x,&y);
if(opt[0] == 'A'){
makeroot(x);makeroot(y);
fa[x] = y;
sz2[y] += sz[x];
}
else if(opt[0] == 'Q'){
makeroot(x);access(y);splay(y);
ch[y][0] = fa[x] = 0;pushup(y);
makeroot(x);makeroot(y);
printf("%lld\n",sz[x] * sz[y]);
makeroot(x);makeroot(y);
fa[x] = y;
sz2[y] += sz[x];
}
}
return 0;
}
CF920E Connected Components?
题目分析:
先转化一下题意:给定 \(n\) 个点 \(m\) 条边的图,问这个图的补图里面有多少个连通分量,以及各自的大小。
我们可以先考虑拿出一个原图中度数最小的点,并且预处理这个点。
然后暴力扫一遍剩下的没被搞在一起的点然后把他们搞在一起。
考虑复杂度分析:
我们最坏的情况显然就是边被平均分配,也就是每个点的相连的边数就是 \(\dfrac{m}{n}\)
考虑对于我们找到最小的点然后预处理就意味着我们只剩下了 \(\dfrac{m}{n}\) 个点,每个点都是 \(\dfrac{m}{n}\) 条相连的边。
因为我们需要每个点都扫一遍所以就是 \(O(n \times \dfrac{m}{n}) = O(m)\)
所以复杂度完全可以过。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 4e5+5;
int fa[N],in[N],bel[N],sz[N];
bool vis[N],flag[N];
vector<int> v[N];
int find(int x){
if(fa[x] == x) return x;
return fa[x] = find(fa[x]);
}
void merge(int x,int y){
fa[find(x)] = find(y);
}
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i=1; i<=m; i++){
int x,y;
scanf("%d%d",&x,&y);
in[x]++;in[y]++;
v[x].push_back(y);v[y].push_back(x);
}
for(int i=1; i<=n; i++) fa[i] = i;
int mn = 1;
for(int i=2; i<=n; i++)
if(in[mn] > in[i]) mn = i;
for(int i : v[mn]) vis[i] = true;
for(int i=1; i<=n; i++) if(!vis[i]) merge(i,mn); //补图
for(int i=1; i<=n; i++){
if(!vis[i] || i == mn) continue;
memset(flag,false,sizeof(flag));
for(int j : v[i]) flag[j] = true;
for(int j=1; j<=n; j++){
if(!flag[j])
merge(j,i);
}
}
int tot = 0;
for(int i=1; i<=n; i++){
if(fa[i] == i) bel[i] = ++tot;
}
for(int i=1; i<=n; i++){
sz[bel[find(i)]]++;
}
sort(sz+1,sz+tot+1);
printf("%d\n",tot);
for(int i=1; i<=tot; i++){
printf("%d ",sz[i]);
}
return 0;
}
总结:
要善于简化题意为更形象的表示方式。
CF920G List Of Integers
题目分析:
显然可以考虑二分 \(x\)。
然后问题就转化为了求解 \([1,x]\) 中有多少数与 \(p\) 互质,也就是求解下面这个式子:
这个式子长着就非常莫反,所以也就套路转化了
那么直接 \(O(\sqrt{n})\) 枚举约数就好了。
所以总复杂度为:\(O(q\log n \sqrt{n})\)
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+5;
const int INF = 1e7+5;
int mu[N],cnt,prime[N];
bool flag[N];
void pre_work(int mx){
mu[1] = 1;
for(int i=2; i<=mx; i++){
if(!flag[i]){
mu[i] = -1;
prime[++cnt] = i;
}
for(int j=1; j<=cnt && prime[j] * i <= mx; j++){
flag[prime[j] * i] = true;
if(i % prime[j] != 0) mu[i * prime[j]] = -mu[i];
else break;
}
}
}
int get_ans(int x,int p){
int ans = 0;
for(int i=1; i * i <= p; i++){
if(p % i == 0){
ans += mu[i] * (x / i);
if(i * i != p){
ans += mu[p / i] * (x / (p / i));
}
}
}
return ans;
}
int main(){
pre_work(1000000);
int t;
scanf("%d",&t);
while(t--){
int x,p,k;
scanf("%d%d%d",&x,&p,&k);
int tmp = get_ans(x,p);
int l = 0,r = INF;
int ans = INF;
while(l <= r){
int mid = (l + r)>>1;
int h = get_ans(mid,p);
if(h - tmp >= k) ans = mid,r = mid - 1;
else l = mid + 1;
}
printf("%d\n",ans);
}
return 0;
}
CF337D Book of Evil
题目分析:
显然我们需要维护 \(dp[i]\) 表示以 \(i\) 为根的子树,到 \(i\) 点距离最远的点的距离。
同理我们需要维护 \(dis[i]\) 表示从 \(i\) 到 \(i\) 子树以外的点的最远的点的距离。
为了方便维护 \(dis\) 我们就在 \(dp\) 里面记两维,即 \(dp[i][0]\) 代表到 \(i\) 的最远距离,\(dp[i][1]\) 代表到 \(i\) 的次远距离。
注意这里的次远并不是严格次远。
然后直接两遍 dfs 维护一下就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
const int INF = 1e9+5;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N];
int cnt,dp[N][2],dis[N],head[N];
bool flag[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void dfs(int now,int fath){
if(flag[now]) dp[now][0] = 0,dp[now][1] = 0;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
dfs(to,now);
if(dp[to][0] + 1 > dp[now][0]){
swap(dp[now][0],dp[now][1]);
dp[now][0] = dp[to][0] + 1;
}
else dp[now][1] = max(dp[now][1],dp[to][0] + 1);
}
}
void get(int now,int fath){
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
if(dp[now][0] == dp[to][0] + 1) dis[to] = max(dis[now] + 1,dp[now][1] + 1);
else dis[to] = max(dis[now] + 1,dp[now][0] + 1);
get(to,now);
}
}
int main()
memset(dis,-0x3f,sizeof(dis));memset(dp,-0x3f,sizeof(dp));
int n,m,d;
scanf("%d%d%d",&n,&m,&d);
for(int i=1; i<=m; i++){
int u;
scanf("%d",&u);
flag[u] = true;
}
for(int i=1; i<n; i++){
int u,v;
scanf("%d%d",&u,&v);
add_edge(u,v);add_edge(v,u);
}
dfs(1,0);
get(1,0);
int ans = 0;
for(int i=1; i<=n; i++){
if(dp[i][0] <= d && dis[i] <= d)
ans++;
}
printf("%d\n",ans);
return 0;
}
CF682C Alyona and the Tree
题目分析:
我们显然可以维护 \(sum[i]\) 表示从 \(i\) 的父亲到 \(i\) 的最远距离。
我们也可以发现当存在某个点 \(x\) 使得 \(sum[x] > a[x]\),那么这一整棵子树都应该删掉。
所以维护出来 \(sum\) 数组之后,直接从根开始 dfs 判断就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e6+5;
struct edge{
int nxt,to,val;
edge(){}
edge(int _nxt,int _to,int _val){
nxt = _nxt,to = _to,val = _val;
}
}e[N];
int cnt,sum[N],sz[N],head[N],fa[N],a[N];
void add_edge(int from,int to,int val){
e[++cnt] = edge(head[from],to,val);
head[from] = cnt;
}
void dfs(int now){
if(sum[now] < 0) sum[now] = 0; //注意 a 一定大于 0
sz[now] = 1;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fa[now]) continue;
sum[to] = sum[now] + e[i].val;
dfs(to);
sz[now] += sz[to];
}
}
int get_ans(int now){
int ans = 0;
if(sum[now] > a[now]) return sz[now];
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fa[now]) continue;
ans += get_ans(to);
}
return ans;
}
signed main(){
int n;
scanf("%lld",&n);
for(int i=1; i<=n; i++) scanf("%lld",&a[i]);
for(int i=2; i<=n; i++){
int val;
scanf("%lld%lld",&fa[i],&val);
add_edge(fa[i],i,val);
}
dfs(1);
printf("%lld\n",get_ans(1));
return 0;
}
CF739B Alyona and a tree
题目分析:
我们会发现我们直接正向的找到每一个点可以控制的点有点难,所以就考虑反向,让每个点找到能控制他的点。
也就是与他距离不超过 \(a_x\) 的所有父亲节点。
当我们可以找到 \(x\) 的最远的一个父亲使得 \(x\) 到他的距离不超过 \(a_x\) 时,也就意味着从这个父亲到 \(x\) 的直接父亲都可以控制 \(x\),那么就可以树上差分快速求解。
对于这样的父亲怎么找?树上倍增向上跳就可以了。
时间复杂度:\(O(n \log n)\)
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6+5;
struct edge{
int nxt,to,val;
edge(){}
edge(int _nxt,int _to,int _val){
nxt = _nxt,to = _to,val = _val;
}
}e[N];
int cnt,n,dis[N],ans[N],fa[N][23],head[N],a[N];
void add_edge(int from,int to,int val){
e[++cnt] = edge(head[from],to,val);
head[from] = cnt;
}
void dfs(int now,int fath){
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
dis[to] = dis[now] + e[i].val;
dfs(to,now);
}
}
void pre_work(){
for(int i=1; i<=20; i++){
for(int j=1; j<=n; j++){
fa[j][i] = fa[fa[j][i-1]][i-1];
}
}
}
void get_ans(int now,int fath){
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
get_ans(to,now);
ans[now] += ans[to];
}
}
signed main(){
scanf("%lld",&n);
for(int i=1; i<=n; i++) scanf("%lld",&a[i]);
for(int i=2; i<=n; i++){
int val;
scanf("%lld%lld",&fa[i][0],&val);
add_edge(fa[i][0],i,val);
}
dfs(1,0);
pre_work();
for(int i=1; i<=n; i++){ //从每个点向上跳
int now = i;
for(int j=20; j>=0; j--){
if(dis[i] - dis[fa[now][j]] <= a[i])
now = fa[now][j];
}
ans[fa[i][0]]++;ans[fa[now][0]]--;
}
get_ans(1,0);
for(int i=1; i<=n; i++){
printf("%lld ",ans[i]);
}
return 0;
}
总结:
要灵活地使用倍增。逆向思维的重要性。
CF782C Andryusha and Colored Balloons
题目分析:
我们可以发现:我们的最多使用的颜色数就是度数最多的节点的度数加一。(不知道为什么,当时直接一眼就出来了)
那么我们就考虑直接暴力 dfs 染色就好了。
假设我们现在染色到 \(x\) 的直接儿子,那么会对他们造成影响的只有 \(x\) 和 \(x\) 的父亲,那么把这两个点打上标记其他的就顺着标号就可以了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N];
int cnt,color[N],deg[N],head[N],fa[N];
bool flag[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void update(int now){
int col = 1;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fa[now]) continue;
while(flag[col]) col++;
color[to] = col++;
}
}
void dfs(int now,int fath){
fa[now] = fath;
flag[color[now]] = flag[color[fath]] = true;
update(now);
flag[color[now]] = flag[color[fath]] = false;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
dfs(to,now);
}
}
int main(){
int n;
scanf("%d",&n);
for(int i=1; i<n; i++){
int u,v;
scanf("%d%d",&u,&v);
add_edge(u,v);add_edge(v,u);
deg[u]++;deg[v]++;
}
int ans = 0;
for(int i=1; i<=n; i++) ans = max(ans,deg[i]);
printf("%d\n",ans + 1);
color[1] = 1;
dfs(1,0);
for(int i=1; i<=n; i++){
printf("%d ",color[i]);
}
return 0;
}
CF813C The Tag Game
题目分析:
我们会发现对于 \(A\) 他一定是一直向着 \(B\) 所在的子树走,而对于 \(B\) 一直向下走。
但是这样最优吗?不一定啊。
我们可能 \(B\) 一开始向上走了一点然后拐弯再向下走,可能依旧可以多拖延一会。
所以我们就直接枚举 \(B\) 从哪里拐弯,然后计算贡献就好了。
对于这个贡献:我们考虑在 \(A\) 走到最低端的时候,因为是交换着行走所以 \(B\) 可能在走也可能不动,但是依旧会导致时间乘二。
代码里的 \(down\) 便是从这个点最多能向下走多远。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N];
int n,x,ans,cnt,down[N],fa[N],dep[N],head[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void dfs(int now,int fath){
dep[now] = dep[fath] + 1;
fa[now] = fath;
for(int i = head[now]; i; i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
dfs(to,now);
down[now] = max(down[now],down[to] + 1);
}
}
void get_ans(int now){
if(!now) return;
if(dep[x] - dep[now] >= dep[now] - dep[1]) return;
ans = max(ans,(dep[now] - dep[1] + down[now]) * 2);
get_ans(fa[now]);
}
int main(){
scanf("%d%d",&n,&x);
for(int i=1; i<n; i++){
int from,to;
scanf("%d%d",&from,&to);
add_edge(from,to);add_edge(to,from);
}
dfs(1,0);
ans = dep[x] - dep[1];
get_ans(x);
printf("%d\n",ans);
return 0;
}
总结:
此类题目一定是先找到最优是怎么做,然后再考虑接下来的事。
CF822D My pretty girl Noora
题目分析:
我们显然可以设 \(f[i]\) 表示初始为 \(i\) 个人最少需要比较多少次。
那么我们的转移就是:
下面就是两个思路:
(一)我们考虑怎么转移最优:
假设我们当前数是 \(6\),我们有两种选择:直接 \(6\)、一个 \(3\) 一个 \(2\)。
带入我们式子中会发现,一定是我们每次选择尽可能小的一个数去转移是最优的。
也就是我们要预处理出来每一个数的最小质因子,然后每次就直接减去最小质因子。
复杂度:\(O(n)\)。
(竟然爆踩标算)
(二)考虑怎么优化转移:
我们如果直接枚举因数是 \(O(\sqrt{n})\) 的复杂度,因为会有很多没用的值。
所以我们就考虑直接枚举这个数的倍数,用这个数去转移到这个数的倍数。
复杂度:\(O(n \log n)\)
卡一卡可能可以过,但是我没过。
代码:
思路一:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MOD = 1e9+7;
const int N = 6e6+5;
int cnt,mn[N],prime[N],f[N];
bool flag[N];
void pre_work(int mx){
mn[1] = 1;
for(int i=2; i<=mx; i++){
if(!flag[i]){
prime[++cnt] = i;
mn[i] = i;
}
for(int j=1; j<=cnt && prime[j] * i <= mx; j++){
flag[prime[j] * i] = true;
mn[prime[j] * i] = prime[j];
if(i % prime[j] == 0) break;
}
}
}
int power(int a,int b){
int res = 1;
while(b){
if(b & 1) res = (res * a) % MOD;
a = (a * a) % MOD;
b>>=1;
}
return res;
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int t,l,r;
scanf("%lld%lld%lld",&t,&l,&r);
pre_work(r);
int inv = power(2,MOD-2);
f[1] = 0;
for(int i=2; i<=r; i++){
f[i] = (((i * (mn[i] - 1))%MOD * inv)%MOD + f[i / mn[i]])%MOD;
}
int now = 1;
int ans = 0;
for(int i=l; i<=r; i++){
ans = (ans + (now * f[i])%MOD)%MOD;
now = (now * t)%MOD;
}
printf("%lld\n",ans);
return 0;
}
思路二:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 6e6+5;
const int MOD = 1e9+7;
int f[N];
int power(int a,int b){
int res = 1;
while(b){
if(b & 1) res = (res * a) % MOD;
a = (a * a) % MOD;
b>>=1;
}
return res;
}
signed main(){
memset(f,0x3f,sizeof(f));
int t,l,r;
scanf("%lld%lld%lld",&t,&l,&r);
f[1] = 0;
for(int i=1; i<=r; i++){
for(int j=2; j * i <= r; j++){
f[j * i] = min(f[i * j],f[i] +1ll * j * i * (j-1) / 2);
}
}
int ans = 0;
int tmp = 1;
for(int i=l; i<=r; i++){
ans = (ans + (tmp * (f[i]%MOD))%MOD)%MOD;
tmp = (tmp * t)%MOD;
}
printf("%lld\n",ans);
}
总结:
要注意一题多解。
对于思路一显然就是性质的发掘,而对于性质二确是更普遍的优化复杂度的方法。
CF817C Really Big Numbers
题目分析:
我们会发现本题满足二分单调性。
证明:
假设我们 \(x\) 满足条件,要证明大于 \(x\) 的数均满足条件。
我们假设某个数 \(y > x\) 且 \(y = x + 10^z\)
那么其实就是相当于对 \(y\) 变化之后对比 \(x\) 变化之后的值,一定不会减少。
因为 \(10^z \ge 1\),所以减去 \(1\) 一定大于等于 \(0\)。
那么也很显然,当 \(y\) 相对于 \(x\) 不止变化一个数位时也显然成立。
也就是一定满足二分单调性。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,s;
int check(int x){
int tmp = x;
while(x){
tmp -= x % 10;
x /= 10;
}
return tmp >= s;
}
signed main(){
scanf("%lld%lld",&n,&s);
int l = 1,r = n + 1;
int ans = n + 1;
while(l <= r){
int mid = (l + r)>>1;
if(check(mid)){
ans = mid;
r = mid - 1;
}
else l = mid + 1;
}
printf("%lld\n",n - ans + 1);
return 0;
}
CF814C An impassioned circulation of affection
题目分析:
我们考虑到小写字母的数量很少,所以我们可以考虑预处理出来每个小写字母,恰好改变 \(k\) 个字母的最长的一段。
那么直接枚举将哪一段全部变成这个字母,然后计算一下就好了。
因为我代码里 \(mx\) 一开始是恰好使用 \(j\) 个,但是显然不用恰好使用,用少一点也没关系,所以要与比它小的取 \(\max\)
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1505;
int sum[27][N],mx[27][N + 2],tot[27];
char s[N];
int main(){
int n;
scanf("%d",&n);
scanf("%s",s+1);
for(int i=1; i<=n; i++){
for(int j=1; j<=26; j++)
sum[j][i] = sum[j][i-1];
sum[s[i] -'a' + 1][i]++;
}
for(int i=1; i<=26; i++){
for(int l=1; l<=n; l++){
for(int r=l; r<=n; r++){
int w = r-l+1-(sum[i][r]-sum[i][l-1]);
mx[i][w] = max(mx[i][w],r-l+1);
}
}
}
for(int i=1; i<=26; i++){
for(int j=1; j<=n; j++){
mx[i][j] = max(mx[i][j],mx[i][j-1]);
}
}
int q;
scanf("%d",&q);
while(q--){
int u;
char g;
cin>>u>>g;
printf("%d\n",mx[g - 'a' + 1][u]);
}
return 0;
}
9.20
POJ3352 Road Construction
题目分析:
将原图进行边双的缩点之后,答案显然就是 \(\lceil \dfrac{叶子节点数量}{2} \rceil\)。
因为我们在叶子节点之间两两匹配之后,显然可以满足条件。
代码:
点击查看代码
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
const int N = 1e5+5;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N];
int cnt,tot,top,col,deg[N],from[N],to[N],dfn[N],low[N],st[N],head[N],color[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void tarjan(int now,int fath){
dfn[now] = low[now] = ++tot;
st[++top] = now;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
if(!dfn[to]){
tarjan(to,now);
low[now] = min(low[now],low[to]);
}
else if(!color[to]){
low[now] = min(low[now],dfn[to]);
}
}
if(low[now] == dfn[now]){
++col;
color[now] = col;
while(st[top] != now){
color[st[top]] = col;
--top;
}
--top;
}
}
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i=1; i<=m; i++){
scanf("%d%d",&from[i],&to[i]);
add_edge(from[i],to[i]);add_edge(to[i],from[i]);
}
for(int i=1; i<=n; i++){
if(!dfn[i]) tarjan(i,0);
}
for(int i=1; i<=m; i++){
if(color[from[i]] != color[to[i]]){
deg[color[from[i]]]++;
deg[color[to[i]]]++;
}
}
int res = 0;
for(int i=1; i<=col; i++){
res += (deg[i] == 1);
}
printf("%d\n",(res + 1) / 2);
return 0;
}
HDU4582 DFS spanning tree
题目分析:
注意一点:题目中给出的生成树就是从 \(1\) 点开始的 DFS 生成树,所以所有的非树边一定是返祖边。
那么也就是可以理解为每一个非树边对应一段区间,要求每段区间至少选则一条边。
根据贪心地策略,我们可以按深度从大到小排序之后每次选择最靠上的一个,这样显然能尽可能地为后面提供便利。
代码:
点击查看代码
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
const int N = 1e5+5;
struct edge{
int nxt,from,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N],a[N];
int cnt,head[N],dep[N],fa[N];
bool vis[N];
bool cmp(edge a,edge b){
if(dep[a.to] == dep[b.to]) return dep[a.from] > dep[b.from];
return dep[a.to] > dep[b.to];
}
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void dfs(int now,int fath){
fa[now] = fath;dep[now] = dep[fath] + 1;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
dfs(to,now);
}
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n,m;
while(~scanf("%d%d",&n,&m)){
if(n == 0 && m == 0) break;
m = m - (n - 1);
for(int i=1; i<n; i++){
int u,v;
scanf("%d%d",&u,&v);
add_edge(u,v);add_edge(v,u);
}
dfs(1,0);
for(int i=1; i<=m; i++){
scanf("%d%d",&a[i].from,&a[i].to);
if(dep[a[i].from] < dep[a[i].to]) swap(a[i].from,a[i].to);
}
sort(a+1,a+m+1,cmp);
int ans = 0;
for(int i=1; i<=m; i++){
int x = a[i].from,y = a[i].to;
while(fa[x] != y){
if(vis[x]) break;
x = fa[x];
}
if(!vis[x]){
ans++;vis[x] = true;
}
}
printf("%d\n",ans);
memset(vis,false,sizeof(vis));memset(head,0,sizeof(head));cnt = 0;
}
}
9.21
CF1733C Parity Shuffle Sorting
题目分析:
我们可以发现题目的意思就是说:可以将奇偶性相同的变成后面数,将奇偶性不同的变成前面的数。
我们设 \(a_1\) 的奇偶性为 \(p\),设最后一个奇偶性为 \(p\) 的数为 \(a_k\)
那么我们就可以先将所有奇偶性为 \(p\) 的变成 \(a_k\),然后将所有奇偶性不为 \(p\) 变成 \(a_k\)。
也就是可以实现不下降。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
int a[N];
pair<int,int> ans[N];
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int t;
scanf("%d",&t);
while(t--){
int n;
scanf("%d",&n);
for(int i=1; i<=n; i++) scanf("%d",&a[i]);
int p = a[1] % 2;
int pos = 0;
for(int i=n; i>=1; i--){
if(a[i] % 2 == p){
pos = i;
break;
}
}
int tot = 0;
for(int i=1; i<=n; i++){
if(a[i] % 2 == p && i != pos)
ans[++tot] = {i,pos};
}
for(int i=1; i<=n; i++){
if(a[i] % 2 != p){
ans[++tot] = {1,i};
}
}
printf("%d\n",tot);
for(int i=1; i<=tot; i++){
printf("%d %d\n",ans[i].first,ans[i].second);
}
}
return 0;
}
CF1733D1 Zero-One (Easy Version)
题目分析:
可以发现当我们不同位置的数量为一个奇数时无解,因为我们每次只会改变偶数个的值。
那么我们考虑在 D1 里面的关键限制:\(y \le x\)。
也就是说我们一旦可以使用 \(y\) 就不要使用 \(x\)。
当不同位置的数量大于 \(2\) 时我们一定可以两两匹配使用 \(y\),关键是如果等于 \(2\) 呢?
如果这两个位置分别为 \(a_1,a_2\) 如果不相邻可以直接用,如果相邻我们可以引入第三个位置 \(a_3\) 使得 \(a_3\) 与 \(a_1\) 和 \(a_2\) 都不相邻,这样就可以使用两次 \(y\) 代替一次 \(x\),也就是这种情况下我们的代价是 \(\min(2\times y,x)\)
题目分析:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e3+5;
char a[N],b[N];
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int t;
scanf("%lld",&t);
while(t--){
int n,x,y;
vector<int> v;
scanf("%lld%lld%lld",&n,&x,&y);
scanf("%s",a + 1);
scanf("%s",b + 1);
for(int i=1; i<=n; i++){
if(a[i] != b[i])
v.push_back(i);
}
if(v.size() & 1) printf("-1\n");
else if(v.size() == 2){
if(v[1] - v[0] == 1) printf("%lld\n",min(2 * y,x));
else printf("%lld\n",y);
}
else printf("%lld\n",v.size() / 2 * y);
}
return 0;
}
CF1733D2 Zero-One (Hard Version)
题目分析:
对于 \(y \le x\) 我们依旧可以使用 D1 的策略,而对于剩下的显然就只可以 \(DP\) 了。
对于此时我们也可以发现一个新的策略,也就是用 \(x\) 代替 \(y\) 的策略,对于 \(a_1,a_2\) 可以使用 \((a_2 - a_1)\) 次 \(x\) 代替一次 \(y\),也就是一点点推出去。
我们可以设 \(dp[i]\) 表示前 \(i-1\) 个不同的位置已经全部完成的最小代价,然后每次就枚举使用 \(x,y\) 哪种操作然后去转移就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e3+5;
char a[N],b[N];
int dp[N];
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int t;
scanf("%lld",&t);
while(t--){
int n,x,y;
vector<int> v;
scanf("%lld%lld%lld",&n,&x,&y);
scanf("%s",a + 1);
scanf("%s",b + 1);
for(int i=1; i<=n; i++){
if(a[i] != b[i])
v.push_back(i);
}
if(v.size() & 1){
printf("-1\n");
continue;
}
if(y <= x){
if(v.size() == 2){
if(v[1] - v[0] == 1) printf("%lld\n",min(2 * y,x));
else printf("%lld\n",y);
}
else printf("%lld\n",v.size() / 2 * y);
}
else{
memset(dp,0x3f,sizeof(dp));
dp[0] = 0;
int n = v.size();
for(int i=0; i<n; i+=2){
int cur = 0;
for(int j = i+2; j<=n; j+=2){
if(j >= i + 4){
if(v[j-2] - v[i-3] == 1) cur += x * (v[j - 2] - v[j - 3]);
else cur += min(y,x * (v[j-2] - v[j-3]));
}
dp[j] = min(dp[j],dp[i] + cur + min(y,x * (v[j - 1] - v[i])));
}
}
printf("%lld\n",dp[n]);
}
}
return 0;
}
CF1733E Conveyor
题目分析:
我们定义 \(x+y\) 相等的点构成一条对角线,也就是一个斜向右上的线。
所以我们可以发现粘液球每走一步都会走向下一条对角线。
所以每一条对角线都一定最多只有一个粘液球。
所以对于题目中的要求也就是第 \(t\) 秒询问 \(x+y\) 这条对角线上的粘液球的位置。
我们会发现如果在 \(t\) 秒在 \(x+y\) 这条对角线上,那么在第 \(t - x - y\) 秒它就是在 \(1\) 的位置。
那么我们就可以将 \(t-x-y\) 个粘液球和 \(t-x-y+1\) 个粘液球去模拟,最后模拟到 \(x+y\) 这条线上数量不同的一个点也就是我们需要的粘液球所在的位置。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
int a[300],b[300];
signed main(){
int ti;
scanf("%lld",&ti);
while(ti--){
memset(a,0,sizeof(a));memset(b,0,sizeof(b));
int t,x,y;
scanf("%lld%lld%lld",&t,&x,&y);
if(t < x + y){
printf("NO\n");
continue;
}
a[0] = t - x - y;
b[0] = t - x - y + 1;
for(int i=0; i<x+y; i++){ //直接暴力枚举那一条对角线
for(int j=i; j>=0; j--){ //枚举行数,可以理解为出去格子的也算只不过行数列数比较多
a[j+1] += a[j]/2;
a[j] -= a[j]/2;
b[j+1] += b[j]/2;
b[j] -= b[j]/2;
}
}
int px = -1,py = -1;
for(int i=0; i<240; i++){ //唯一一个不一样的地方就是 我们需要的粘液球的位置
if(a[i] != b[i]){
px = i;
py = x + y - i;
break;
}
}
if(x == px && y == py) printf("YES\n");
else printf("NO\n");
}
return 0;
}
CF999E Reachability from the Capital
题目分析:
我们可以考虑对原图进行缩点,并且将 \(s\) 可以到达的点与 \(s\) 缩成一个点。
那么在这个图中我们新加入的边数就很好判断了,显然就是入度为 \(0\) 的点的数量。
因为这些相当于一条链的开始,从这里连边一整条链都可以被到达。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e4+5;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N];
int cnt,col,top,tot,n,m,s,ru[N],chu[N],dp[N],dfn[N],low[N],st[N],color[N],head[N],from[N],to[N];
bool vis[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void tarjan(int now){
dfn[now] = low[now] = ++tot;
st[++top] = now;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(!dfn[to]){
tarjan(to);
low[now] = min(low[now],low[to]);
}
else if(!color[to]){
low[now] = min(low[now],dfn[to]);
}
}
if(low[now] == dfn[now]){
++col;
color[now] = col;
while(st[top] != now){
color[st[top]] = col;
top--;
}
top--;
}
}
void dfs(int now){
vis[now] = true;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(vis[to]) continue;
dfs(to);
add_edge(to,s);
}
}
int main(){
scanf("%d%d%d",&n,&m,&s);
for(int i=1; i<=m; i++){
scanf("%d%d",&from[i],&to[i]);
add_edge(from[i],to[i]);
}
dfs(s); //将与 s 相连的全部缩成一个点
for(int i=1; i<=n; i++){
if(!dfn[i])
tarjan(i);
}
s = color[s];
memset(head,0,sizeof(head));cnt=0;
for(int i=1; i<=m; i++){
if(color[from[i]] != color[to[i]]){
add_edge(color[from[i]],color[to[i]]);
ru[color[to[i]]]++;chu[color[from[i]]]++;
}
}
int ans = 0;
for(int i=1; i<=col; i++){
if(i == s) continue;
if(ru[i] == 0) ans++;
}
printf("%d\n",ans);
return 0;
}
[清华集训2016] 组合数问题
题目分析:
我们发现我们需要算的组合数特别大,而且 \(k\) 还是一个质数,所以就可以考虑卢卡斯定理。
也就是将 \(n,m\) 分别进行二进制分解,分解的结果分别存在 \(bn,bm\) 中,那么也就是下面这个式子:
所谓是 \(k\) 的倍数也就是模 \(k\) 等于 \(0\),因为我们是累乘所以其中只要有一项是 \(0\) 那么就全是 \(0\)。
也就是只要有一项满足 \(bn_i < bm_i\) 那么就是 \(0\)。
直接数位 DP 就可以解决了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MOD = 1e9+7;
const int N = 1000;
int dp[N][2][2][2][2],bm[N],bn[N],k;
int get_ans(int now,bool a,bool b,bool c,bool d){
//当前是第 now 位,是否出现 i < j,i 和 j 是否不完全相同, i 和 n 是否不完全相同,j 和 m 是否不完全相同
if(!now) return a;
if(dp[now][a][b][c][d]) return dp[now][a][b][c][d];
int limitn,limitm;
limitn = c ? (k-1) : bn[now];
limitm = d ? (k-1) : bm[now];
int ans = 0;
for(int i=0; i<=limitn; i++){
for(int j=0; (j <= i || b) && j <= limitm; j++){
ans = (ans + get_ans(now - 1,(i < j) || a,(i != j)||b,(i < bn[now]) || c,(j < bm[now]) || d))%MOD;
}
}
dp[now][a][b][c][d] = ans;
return ans;
}
signed main(){
int t;
scanf("%lld%lld",&t,&k);
while(t--){
int n,m;
scanf("%lld%lld",&n,&m);
int mx = max(n,m);
int len = 0;
while(mx){
len++;
mx /= k;
}
for(int i=1; i<=len; i++) bn[i] = n % k,n /= k;
for(int i=1; i<=len; i++) bm[i] = m % k,m /= k;
printf("%lld\n",get_ans(len,false,false,false,false));
memset(dp,0,sizeof(dp));
}
return 0;
}
总结:
对于很大的组合数的计算,一般都需要考虑卢卡斯定理。
[SDOI2019]移动金币
题目分析:
看到熟悉的人名,大概率是博弈论了。
这种题显然需要将金币之间的间隔视为堆,将间隔的大小视为石子的数量。
那么我们就可以发现我们移动一枚金币相当于将某一堆石子移到右边的石子中,那么也就是一个阶梯博弈。
我们可以将最右边的间隔视为 \(1\) 号,那么也就是所有偶数号的堆中的石子数的异或和不为 \(0\) 先手必胜。
但是不等于这个条件很难维护,我们转化为异或和为 \(0\) 就可以好维护许多,这样也只需要将所有的情况即 \(\binom{n}{m}\) 减掉我们求出来的值就是答案。
我们考虑异或和那么显然需要按位考虑,也就是对于 \(k\) 进制下每一位我们只有偶数个数含有这一位,也就是可以写出一个显然的 \(DP\),即设 \(dp[i][j]\) 表示当前填好了前 \(i\) 位,剩余 \(j\) 个数字没有填的方案数。
我们令 \(h = \lceil \dfrac{m}{2} \rceil\),也就是偶数号堆的数量,那么就可以列出以下转移:
统计答案显然就是维护剩余多少个,而对于剩余的我们就是要分配到奇数号堆里,使用插板法就可以做到。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MOD = 1e9+9;
const int N = 2e5+5;
int fac[N],inv[N],dp[30][N];
int power(int a,int b){
int res = 1;
while(b){
if(b & 1) res = (res * a) % MOD;
a = (a * a) % MOD;
b >>= 1;
}
return res;
}
int mod(int x){
return ((x % MOD)+MOD)%MOD;
}
int C(int n,int m){
if(n < m || m < 0) return 0;
return mod(fac[n] * mod(inv[m] * inv[n - m]));
}
int chaban(int n,int m){ //题目可以为空所以就要转化为不可以为空
if(n == 0 && m == 0) return 1;
return C(n + m - 1,m - 1);
}
void pre_work(int mx){
fac[0] = 1;
for(int i=1; i<=mx; i++) fac[i] = (fac[i-1] * i)%MOD;
inv[mx] = power(fac[mx],MOD-2);
for(int i=mx-1; i>=0; i--) inv[i] = (inv[i+1] * (i+1))%MOD;
}
signed main(){
int n,m;
scanf("%lld%lld",&n,&m);
pre_work(n + m);
int p = 1;
while((1<<p) <= n) p++;
/*
设 dp[i][j] 表示填完了前 i 位,当前剩余 j 个没有填充的方案数
*/
dp[p][n-m] = 1;
int h = (m + 1) / 2;
for(int i=p-1; i>=0; i--){
//k 枚举的当前这一位填多少个
for(int j=0; j<=n-m; j++){
for(int k=0; k<=h && (1<<i) * k + j <= n - m; k+=2){
dp[i][j] = mod(dp[i][j] + mod(dp[i+1][j + (1<<i) * k] * C(h,k)));
}
}
}
int ans = 0;
for(int i=0; i<=n-m; i++) ans = mod(ans + mod(dp[0][i] * chaban(i,(m + 1) - h)));
printf("%lld\n",mod(C(n,m) - ans));
return 0;
}
总结:
此类问题都需要将距离视为堆,大小视为石子个数,然后转化为 Nim 游戏的几个模型之一。
[CEOI2017] Chase
题目分析:
我们显然可以向树形 DP 的方向考虑。
设:
\(a[i]\) 表示 \(i\) 点的权值
\(sum[i]\) 表示与 \(i\) 相邻的边的点权和
\(f[i][j]\) 表示以 \(i\) 为终点,也就是从下到上,恰好放了 \(j\) 个磁铁的贡献
\(g[i][j]\) 表示以 \(i\) 为起点,也就是从上到下,恰好放了 \(j\) 个磁铁的贡献
我们维护答案就是:
\(ans = \max_{g \in son_i} f[i][j] + g[v][m - j]\)
也就是我们枚举以 \(i\) 为中转节点,可能恰好不是最优但是这样确实可以过,如果修改一下也是小改。
所以初始化也就是当前点为起点/终点时的贡献,转移:
对于 \(g\) 因为我们统计答案时默认它只是路途中的节点而不是起点,所以可以直接减掉 \(a[fa[i]]\)。
因为我们从上到下以及从下到上的顺序显然会影响答案,所以需要将儿子逆序再做一遍。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5+5;
const int V = 105;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N];
int n,m,cnt,ans,g[N][V],f[N][V],sum[N],a[N],head[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void dfs(int now,int fath){
for(int i=1; i<=m; i++){
f[now][i] = sum[now],g[now][i] = sum[now] - a[fath];
}
stack<int> st;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
dfs(to,now);st.push(to);
for(int j=1; j<=m; j++) ans = max(ans,f[now][j] + g[to][m - j]);
for(int j=1; j<=m; j++){
f[now][j] = max(f[now][j],max(f[to][j],f[to][j-1] + sum[now] - a[to]));
g[now][j] = max(g[now][j],max(g[to][j],g[to][j-1] + sum[now] - a[fath]));
}
}
for(int i=1; i<=m; i++){
f[now][i] = sum[now],g[now][i] = sum[now] - a[fath];
}
while(!st.empty()){
int to = st.top();st.pop();
for(int j=1; j<=m; j++) ans = max(ans,f[now][j] + g[to][m - j]);
for(int j=1; j<=m; j++){
f[now][j] = max(f[now][j],max(f[to][j],f[to][j-1] + sum[now] - a[to]));
g[now][j] = max(g[now][j],max(g[to][j],g[to][j-1] + sum[now] - a[fath]));
}
}
}
signed main(){
scanf("%lld%lld",&n,&m);
for(int i=1; i<=n; i++) scanf("%lld",&a[i]);
for(int i=1; i<n; i++){
int x,y;
scanf("%lld%lld",&x,&y);
add_edge(x,y);add_edge(y,x);
sum[x] += a[y];sum[y] += a[x];
}
dfs(1,0);
printf("%lld\n",ans);
return 0;
}
9.22
[BalticOI 2009 Day1]甲虫
题目分析:
看到 \(n\) 的范围显然想到区间 \(dp\)
可以显然地想到设 \(dp[l][r][0/1]\) 表示将 \([l,r]\) 内的水滴都喝完,当前在最左边/最右边的最大值。
为了转移需要再记 \(g[l][r][0/1]\) 表示 \(dp[l][r][0/1]\) 的前提下时间的最小值。
但是这样会出现一个问题:我们的 \(dp\) 只是当前最优,可能以后通过 \(g\) 转移的时候 \(g\) 会影响我们的答案。
所以就考虑转换下思维,既然实时算费用不好算就考虑费用提前计算。
为了提前计算我们需要的费用就需要记录我们总共要选的区间长度是多少。
注意:我们可能不存在一个在 \(0\) 位置的水滴,但是我们显然需要从这个地方开始 \(dp\),那么我们就需要新加入一个点位置在 \(0\),但是计算答案的时候需要减去这个点的贡献。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 600;
const int INF = 1e9+5;
int pos,n,m,a[N],dp[N][N][2];
int DP(int limit){
memset(dp,0x3f,sizeof(dp));
dp[pos][pos][0] = dp[pos][pos][1] = 0;
for(int len=2; len <=limit; len++){
for(int l=1; l + len - 1 <= n; l++){
int r = l + len - 1;
dp[l][r][0] = min(dp[l][r][0],min(dp[l+1][r][0] + (a[l+1]-a[l])*(limit-len+1),dp[l+1][r][1] + (a[r]-a[l])*(limit-len+1)));
dp[l][r][1] = min(dp[l][r][1],min(dp[l][r-1][0] + (a[r]-a[l])*(limit-len+1),dp[l][r-1][1] + (a[r]-a[r-1])*(limit-len+1)));
}
}
int tmp = INF;
for(int l=1; l+limit-1<=n; l++){
int r = l + limit - 1;
tmp = min(tmp,min(dp[l][r][0],dp[l][r][1]));
}
return tmp;
}
int main(){
scanf("%d%d",&n,&m);
bool flag = false;
for(int i=1; i<=n; i++){
scanf("%d",&a[i]);
if(a[i] == 0) flag = true;
}
if(!flag) n++;
sort(a+1,a+n+1);
for(int i=1; i<=n; i++) if(a[i] == 0) pos = i;
int ans = -INF;
for(int i=1; i<=n; i++){
ans = max(ans,i * m - DP(i) - (!flag ? m : 0));
}
printf("%d\n",ans);
return 0;
}
总结:
费用提前计算是非常好用的转移方法。
9.23
CF1525C Robot Collisions
题目分析:
我们可以发现只有两个点的奇偶性相同才可以相遇。
因为每走一步奇偶性变化一次,所以如果一开始就不相同就永远也不会相同。
我们可以将 R
视为 )
,将 L
视为 (
,问题就可以转化为一个括号匹配问题。
因为最靠近的显然会先匹配。
我们考虑如果存在不能匹配的怎么办?
我们考虑使用镜像来进行考虑。
我们定义镜像为:对于原序列的一个数 \(i\),其在镜像中的位置为 \(m - i + 1\)。
那么对于 L
无法匹配就可以理解为从 \(-x\) 开始向右走。
那么对于 R
无法匹配就可以理解为从 \(2 \times m - x\) 开始向左走。
需要注意:奇数和偶数分开讨论
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
struct node{
int now,pos;
bool flag;
}a[N];
int n,m,top,ans[N];
node st[N];
bool cmp(node a,node b){
return a.now < b.now;
}
void calc(int k){
top = 0;
for(int i=1; i<=n; i++){
if(a[i].now % 2 != k) continue;
if(a[i].flag){
if(top == 0){
a[i].now = -a[i].now;a[i].flag = !a[i].flag;
st[++top] = a[i];
}
else{
ans[a[i].pos] = ans[st[top].pos] = (a[i].now - st[top].now) / 2;
top--;
}
}
else{
st[++top] = a[i];
}
}
if(top & 1){
for(int i=1; i<top; i++){ //去掉最后的 st[top] 因为不可能匹配
st[i] = st[i+1];
}
top--;
}
for(int i=1; i<=top; i+=2){
st[i].now = 2 * m - st[i].now;
}
for(int i=1; i<=top; i+=2){
ans[st[i].pos] = ans[st[i+1].pos] = (st[i].now - st[i+1].now) / 2;
}
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int t;
scanf("%d",&t);
while(t--){
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++) ans[i] = -1;
for(int i=1; i<=n; i++){
a[i].pos = i;
scanf("%d",&a[i].now);
}
for(int i=1; i<=n; i++){
char c;
cin>>c;
a[i].flag = (c == 'L');
}
sort(a+1,a+n+1,cmp);
calc(1);calc(0);
for(int i=1; i<=n; i++){
printf("%d ",ans[i]);
}
printf("\n");
}
return 0;
}
CF1525D Armchairs
题目分析:
一个贪心:每个人找到最近的一个凳子去坐。
但是这个显然是错误的,比如下面的这个数据:
0 1 0 1
显然选择最近的不对。
但是这样我们就可以发现一个新的贪心策略:
对于两个人 \(a < b\),和两个椅子 \(c < d\),显然 \((a,c)(b,d)\) 是最优的。
所以也就是可以设 \(dp[i][j]\) 表示前 \(i\) 个人匹配前 \(j\) 把椅子的最小总代价。
转移也就是:
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 5005;
int g,h,a[N],b[2][N],f[N][N];
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
memset(f,0x3f,sizeof(f));
int n;
scanf("%d",&n);
for(int i=1; i<=n; i++){
scanf("%d",&a[i]);
if(a[i] == 0) b[0][++g] = i;
if(a[i] == 1) b[1][++h] = i;
}
for(int i=0; i<=g; i++) f[0][i] = 0;
for(int i=1; i<=h; i++){
for(int j=i; j<=g; j++){
f[i][j] = min(f[i][j-1],f[i-1][j-1] + abs(b[1][i] - b[0][j]));
}
}
printf("%d\n",f[h][g]);
}
CF1525E Assimilation IV
题目分析:
我们可以发现统一计算所有点的期望很难,所以就考虑根据期望的线性性分开求每一个点的期望。
我们会发现每一种情况其实就是一种排列,也就是计算有多少种排列使得这个点可以被点亮。
但是发现仿佛不好写,所以就考虑使用补集,也就是找有多少种排列使得这个点不可以被点亮。
也就是对于任意一个位置 \(k\),都必须满足 \(k\) 上的这个城市到这个点的距离大于 \(n - k + 1\)。
也就是光走不到。
那么对于总的方案数就是用乘法原理搞一下就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MOD = 998244353;
const int M = 5e4+5;
const int N = 50;
int cnt[M][N];
int power(int a,int b){
int res = 1;
while(b){
if(b & 1) res = (res * a)%MOD;
a = (a * a)%MOD;
b >>= 1;
}
return res;
}
int mod(int x){
return ((x % MOD) + MOD)%MOD;
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n,m;
scanf("%lld%lld",&n,&m);
for(int i=1; i<=n; i++){
for(int j=1; j<=m; j++){
int x;
scanf("%lld",&x);
cnt[j][x]++;
}
}
int p = 1;
for(int i=1; i<=n; i++) p = (p * i)%MOD;
int ans = 0;
for(int i=1; i<=m; i++){ //枚举每一个点的期望
int sum = 0,tmp = 1;
for(int j=n; j>=1; j--){
sum = mod(sum + cnt[i][j+1]);
tmp = (tmp * sum)%MOD;
sum = mod(sum - 1);
}
ans = (ans + (mod(p - tmp) * power(p,MOD-2)))%MOD;
}
printf("%lld\n",ans);
return 0;
}
总结:
整体的期望不好计算那么就分开计算最后累计。
[HEOI2016/TJOI2016]排序
题目分析:
考虑我们只需要知道一个位置的值,那么我们就尝试是否可以通过枚举然后判断是否可行。
其实是可以做的。
考虑假设我们给定了一个 \(01\) 序列,我们排序是很好弄的。
从低到高就是将 \(1\) 放到后面,从高到低就是将 \(1\) 放到前面。
我们也这样来转化:将大于等于我们枚举值的数设为 \(1\),将其余数设为 \(0\)。
这样就只需要判断排序完成之后当前位置的值是多少就好了,此时就可以显然地发现满足二分单调性所以直接二分就好了。
复杂度:\(O(n \log^2 n)\)
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
int p,n,m,a[N],b[N],sum[4*N],tag[4*N];
struct Query{
int opt,l,r;
}q[N];
void pushup(int now){
sum[now] = sum[now<<1] + sum[now<<1|1];
}
void pushdown(int now,int l,int r){
if(tag[now] != -1){
int mid = (l + r)>>1;
tag[now<<1] = tag[now];tag[now<<1|1] = tag[now];
sum[now<<1] = tag[now] * (mid - l + 1);
sum[now<<1|1] = tag[now] * (r - mid);
tag[now] = -1;
}
}
void build(int now,int now_l,int now_r){
if(now_l == now_r){
sum[now] = b[now_l];
return;
}
int mid = (now_l + now_r)>>1;
build(now<<1,now_l,mid);
build(now<<1|1,mid+1,now_r);
pushup(now);
}
void change(int now,int now_l,int now_r,int l,int r,int val){
if(l > r) return;
if(l <= now_l && r >= now_r){
tag[now] = val;
sum[now] = (now_r - now_l + 1) * val;
return;
}
pushdown(now,now_l,now_r);
int mid = (now_l + now_r)>>1;
if(l <= mid) change(now<<1,now_l,mid,l,r,val);
if(r > mid) change(now<<1|1,mid+1,now_r,l,r,val);
pushup(now);
}
int query(int now,int now_l,int now_r,int l,int r){
if(l > r) return 0;
if(l <= now_l && r >= now_r){
return sum[now];
}
pushdown(now,now_l,now_r);
int mid = (now_l + now_r)>>1;
int ans = 0;
if(l <= mid) ans += query(now<<1,now_l,mid,l,r);
if(r > mid) ans += query(now<<1|1,mid+1,now_r,l,r);
pushup(now);
return ans;
}
bool check(int limit){
memset(tag,-1,sizeof(tag));
for(int i=1; i<=n; i++) b[i] = a[i] >= limit;
build(1,1,n);
for(int i=1; i<=m; i++){
int k = query(1,1,n,q[i].l,q[i].r); //寻找 1 的个数
if(q[i].opt == 0){
change(1,1,n,q[i].l,q[i].r-k,0);
change(1,1,n,q[i].r-k+1,q[i].r,1);
}
else if(q[i].opt == 1){
change(1,1,n,q[i].l,q[i].l+k-1,1);
change(1,1,n,q[i].l+k,q[i].r,0);
}
}
return query(1,1,n,p,p);
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++)
scanf("%d",&a[i]);
for(int i=1; i<=m; i++){
scanf("%d%d%d",&q[i].opt,&q[i].l,&q[i].r);
}
scanf("%d",&p);
int l = 1,r = n;
int ans = 0;
while(l <= r){
int mid = (l + r)>>1;
if(check(mid)){
ans = mid;
l = mid + 1;
}
else r = mid - 1;
}
printf("%d\n",ans);
return 0;
}
总结:
通过枚举或者二分将复杂的问题化简。
HDU5294 KPI
题目分析:
显然最关键的操作是寻找中位数。
我们就可以考虑建一棵权值线段树,每次在线段树上二分就可以找到中位数。
对于插入和删除就是线段树上的单点修改。
代码:
点击查看代码
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<queue>
#include<iostream>
using namespace std;
const int N = 1e4+5;
struct node{
char opt[10];
int x;
}a[N];
int tot,sz[4 * N],b[N];
void pushup(int now){
sz[now] = sz[now<<1] + sz[now<<1|1];
}
void modify(int now,int now_l,int now_r,int pos,int val){
if(now_l == now_r){
sz[now] += val;
return;
}
int mid = (now_l + now_r)>>1;
if(pos <= mid) modify(now<<1,now_l,mid,pos,val);
else modify(now<<1|1,mid+1,now_r,pos,val);
pushup(now);
}
int query(int now,int now_l,int now_r,int k){
if(now_l == now_r) return now_l;
int mid = (now_l + now_r)>>1;
if(sz[now<<1] >= k) return query(now<<1,now_l,mid,k);
return query(now<<1|1,mid+1,now_r,k - sz[now<<1]);
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n;
int cnt = 0;
while(~scanf("%d",&n)){
++cnt;
printf("Case #%d:\n",cnt);
tot = 0;
for(int i=1; i<=n; i++){
scanf("%s",a[i].opt + 1);
if(a[i].opt[1] == 'i') scanf("%d",&a[i].x),b[++tot] = a[i].x;
}
sort(b+1,b+tot+1);
tot = unique(b+1,b+tot+1) - b - 1;
for(int i=1; i<=n; i++){
if(a[i].opt[1] == 'i')
a[i].x = lower_bound(b+1,b+tot+1,a[i].x) - b;
}
queue<int> q;
for(int i=1; i<=n; i++){
if(a[i].opt[1] == 'i'){
q.push(a[i].x);
modify(1,1,tot,a[i].x,1);
}
else if(a[i].opt[1] == 'o'){
modify(1,1,tot,q.front(),-1);
q.pop();
}
else if(a[i].opt[1] == 'q'){
printf("%d\n",b[query(1,1,tot,(q.size() / 2) + 1)]);
}
}
while(!q.empty()){
modify(1,1,tot,q.front(),-1);
q.pop();
}
}
return 0;
}
CF85D Sum of Medians
题目分析:
因为是从小到大排序我们就可以考虑建一棵权值线段树,因为权值线段树就是从小到大排序的。
我们可以维护 \(sum[i][j]\) 表示 \(i\) 这个区间模 \(5\) 等于 \(j\) 的位置上的数的和。
那么区间合并就是直接加上左区间的以及判断一下左区间对右区间的影响就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5+5;
struct node{
char opt[10];
int x;
}q[N];
int tot,b[N],sz[4 * N],sum[4 * N][5];
void pushup(int now){
sz[now] = sz[now<<1] + sz[now<<1|1];
int p = sz[now<<1] % 5;
for(int i=0; i<=4; i++){
sum[now][i] = sum[now<<1][i];
sum[now][i] += sum[now<<1|1][((i - p)%5 + 5)%5];
}
}
void modify(int now,int now_l,int now_r,int pos,int val){
if(now_l == now_r){
sz[now] += val;
for(int i=0; i<=4; i++) sum[now][i] = (sz[now] / 5) * b[now_l];
for(int i=1; i<=sz[now]%5; i++) sum[now][i] += b[now_l];
return;
}
int mid = (now_l + now_r)>>1;
if(pos <= mid) modify(now<<1,now_l,mid,pos,val);
else modify(now<<1|1,mid+1,now_r,pos,val);
pushup(now);
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n;
scanf("%lld",&n);
for(int i=1; i<=n; i++){
scanf("%s",q[i].opt + 1);
if(q[i].opt[1] != 's'){
scanf("%lld",&q[i].x);
b[++tot] = q[i].x;
}
}
sort(b+1,b+tot+1);
tot = unique(b+1,b+tot+1) - b - 1;
for(int i=1; i<=n; i++){
if(q[i].opt[1] != 's'){
q[i].x = lower_bound(b+1,b+tot+1,q[i].x) - b;
}
}
for(int i=1; i<=n; i++){
if(q[i].opt[1] == 'a'){
modify(1,1,tot,q[i].x,1);
}
else if(q[i].opt[1] == 'd'){
modify(1,1,tot,q[i].x,-1);
}
else if(q[i].opt[1] == 's'){
printf("%lld\n",sum[1][3]);
}
}
return 0;
}
CF482B Interesting Array
题目分析:
对于这类操作我们肯定先考虑按位来做。
对于一个与 \(x\) 的操作,其实就是相当于让 \(x\) 二进制下的每一个 \(1\) 区间里的所有数都有,其实就是一个或操作。
那么我们就是将所有的限制搞完之后再重新判断一遍是否符合条件就好了。
对于区间或操作以及询问我们可以考虑记 \(sum[i]\) 表示 \(i\) 这个区间内所有数与的值,然后这个题就变得很简单了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5+5;
const int III = (1ll<<31) - 1;
struct node{
int l,r,x;
}q[N];
int ans[4 * N],tag[4 * N];
void pushup(int now){
ans[now] = ans[now<<1] & ans[now<<1|1];
}
void pushdown(int now){
tag[now<<1] |= tag[now];
tag[now<<1|1] |= tag[now];
ans[now<<1] |= tag[now];
ans[now<<1|1] |= tag[now];
tag[now] = 0;
}
void change(int now,int now_l,int now_r,int l,int r,int x){
if(l <= now_l && now_r <= r){
ans[now] |= x;
tag[now] |= x;
return;
}
pushdown(now);
int mid = (now_l + now_r)>>1;
if(l <= mid) change(now<<1,now_l,mid,l,r,x);
if(r > mid) change(now<<1|1,mid+1,now_r,l,r,x);
pushup(now);
}
int query(int now,int now_l,int now_r,int l,int r){
if(l <= now_l && now_r <= r)
return ans[now];
pushdown(now);
int tmp = III;
int mid = (now_l + now_r)>>1;
if(l <= mid) tmp = tmp & query(now<<1,now_l,mid,l,r);
if(r > mid) tmp = tmp & query(now<<1|1,mid+1,now_r,l,r);
pushup(now);
return tmp;
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n,m;
scanf("%lld%lld",&n,&m);
bool flag = true;
for(int i=1; i<=m; i++){
scanf("%lld%lld%lld",&q[i].l,&q[i].r,&q[i].x);
change(1,1,n,q[i].l,q[i].r,q[i].x);
}
for(int i=1; i<=m; i++){
int p = query(1,1,n,q[i].l,q[i].r);
if(p != q[i].x) flag = false;
}
if(flag){
printf("YES\n");
for(int i=1; i<=n; i++){
printf("%lld ",query(1,1,n,i,i));
}
}
else{
printf("NO\n");
}
printf("\n");
return 0;
}
CF383C Propagating tree
题目分析:
考虑这类题目我们都要将对于与子树的相对深度有关的修改转化为与子树绝对深度有关的修改。
我们发现不用考虑下传数值,每次查询这个点到根路径上的信息就好了。
这里我们考虑,对于一个深度为偶数的点,那么距离它为奇数的点造成的贡献为负值,对于距离他为偶数的点造成的贡献为正值。
而对于一个深度为奇数的点则正好相反。
所以我们就可以考虑对于深度为奇数的点加权值的相反数,对于深度为偶数的点加相应的权值。
这样查询的时候深度为偶数的点不用管,深度为奇数的点取反就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5+5;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N * 2];
int tot,cnt,head[N],sum[4*N],top[N],id[N],b[N],a[N],fa[N],dep[N],sz[N],son[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void dfs1(int now,int fath){
dep[now] = dep[fath] + 1;fa[now] = fath;
sz[now] = 1;son[now] = 0;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
dfs1(to,now);
sz[now] += sz[to];
if(sz[to] > sz[son[now]]) son[now] = to;
}
}
void dfs2(int now,int topf){
top[now] = topf;id[now] = ++tot;
if(dep[now] & 1) b[tot] = a[now];
else b[tot] = -a[now];
if(son[now]) dfs2(son[now],topf);
for(int i = head[now]; i; i = e[i].nxt){
int to = e[i].to;
if(to == fa[now] || to == son[now]) continue;
dfs2(to,to);
}
}
void pushup(int now){
sum[now] = sum[now<<1] + sum[now<<1|1];
}
void build(int now,int now_l,int now_r){
if(now_l == now_r){
sum[now] = b[now_l];
return;
}
int mid = (now_l + now_r)>>1;
build(now<<1,now_l,mid);
build(now<<1|1,mid+1,now_r);
pushup(now);
}
void modify(int now,int now_l,int now_r,int pos,int val){
if(now_l == now_r){
sum[now] += val;
return;
}
int mid = (now_l + now_r)>>1;
if(pos <= mid) modify(now<<1,now_l,mid,pos,val);
if(pos > mid) modify(now<<1|1,mid+1,now_r,pos,val);
pushup(now);
}
int query(int now,int now_l,int now_r,int l,int r){
if(l <= now_l && r >= now_r){
return sum[now];
}
int mid = (now_l + now_r)>>1;
int ans = 0;
if(l <= mid) ans += query(now<<1,now_l,mid,l,r);
if(r > mid) ans += query(now<<1|1,mid+1,now_r,l,r);
return ans;
}
int Query(int x,int y){
int ans = 0;
while(top[x] != top[y]){
if(dep[top[x]] < dep[top[y]]) swap(x,y);
ans += query(1,1,tot,id[top[x]],id[x]);
x = fa[top[x]];
}
if(dep[x] > dep[y]) swap(x,y);
ans += query(1,1,tot,id[x],id[y]);
return ans;
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n,m;
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++) scanf("%d",&a[i]);
for(int i=1; i<n; i++){
int from,to;
scanf("%d%d",&from,&to);
add_edge(from,to);add_edge(to,from);
}
//对于深度为奇数的节点权值 + val;
//对于深度为偶数的节点权值 - val;
dfs1(1,0);
dfs2(1,1);
for(int i=1; i<=m; i++){
int opt,x,y;
scanf("%d",&opt);
if(opt == 1){
scanf("%d%d",&x,&y);
if(dep[x] & 1) modify(1,1,tot,id[x],y);
else modify(1,1,tot,id[x],-y);
}
else{
scanf("%d",&x);
int ans = Query(1,x);
if(dep[x] & 1) printf("%d\n",ans + a[x]);
else printf("%d\n",-ans + a[x]);
}
}
return 0;
}
总结:
对于此类题目都是将相对的深度转化为绝对的深度就好做很多了。
CF833B The Bakery
题目分析:
我们显然可以得到设 \(dp[i][j]\) 表示将前 \(i\) 个序列划分为 \(j\) 段的最大价值。
那么我们考虑每加入一个数会对那些值的转移产生影响。
假设我们加入 \(i\) 位置的数,其上一次出现的位置为 \(pre[i]\)。
那么对于 \([pre[i]-1,i]\) 的所有转移都会产生加一的影响,因为多出现了一个不同数。
那么显然直接用线段树维护一下影响,然后转移就是线段树求区间最大值。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 4e4+5;
const int K = 55;
int lst[N],pre[N],a[N],dp[N][K],mx[N * 4],tag[N * 4];
void pushup(int now){
mx[now] = max(mx[now<<1],mx[now<<1|1]);
}
void build(int now,int now_l,int now_r,int k){
tag[now] = 0;
if(now_l == now_r){
mx[now] = dp[now_l-1][k];
return;
}
int mid = (now_l + now_r)>>1;
build(now<<1,now_l,mid,k);build(now<<1|1,mid+1,now_r,k);
pushup(now);
}
void pushdown(int now){
mx[now<<1] += tag[now];
mx[now<<1|1] += tag[now];
tag[now<<1] += tag[now];tag[now<<1|1] += tag[now];
tag[now] = 0;
}
void update(int now,int now_l,int now_r,int l,int r,int val){
if(l <= now_l && now_r <= r){
mx[now] += val;
tag[now] += val;
return;
}
pushdown(now);
int mid = (now_l + now_r)>>1;
if(l <= mid) update(now<<1,now_l,mid,l,r,val);
if(r > mid) update(now<<1|1,mid+1,now_r,l,r,val);
pushup(now);
}
int query(int now,int now_l,int now_r,int l,int r){
if(l <= now_l && now_r <= r){
return mx[now];
}
int mid = (now_l + now_r)>>1;
int ans = 0;
if(l <= mid) ans = max(ans,query(now<<1,now_l,mid,l,r));
if(r > mid) ans = max(ans,query(now<<1|1,mid+1,now_r,l,r));
return ans;
}
int main(){
int n,k;
scanf("%d%d",&n,&k);
for(int i=1; i<=n; i++){
scanf("%d",&a[i]);
pre[i] = lst[a[i]] + 1;
lst[a[i]] = i;
}
for(int i=1; i<=k; i++){
build(1,1,n,i-1);
for(int j=1; j<=n; j++){
update(1,1,n,pre[j],j,1);
dp[j][i] = query(1,1,n,1,j);
}
}
printf("%d\n",dp[n][k]);
return 0;
}
CF1734C Removing Smallest Multiples
题目分析:
我们显然可以枚举每一次需要删除的数然后用这个数去删除。
需要注意的是需要用这一个数下一个可以删除的数是不能删除的数就应该退出循环,不管用这个数去删了。
根据调和级数这个的复杂度为:\(O(n\log n)\)
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e6+5;
char s[N];
bool flag[N],vis[N];
signed main(){
int t;
scanf("%lld",&t);
while(t--){
int n;
scanf("%lld",&n);
scanf("%s",s+1);
for(int i=1; i<=n; i++){
flag[i] = (s[i] == '0');
vis[i] = (s[i] == '0');
}
int ans = 0;
for(int i=1; i<=n; i++){
if(vis[i]){
for(int j=i; j<=n; j+=i){
if(flag[j]){
flag[j] = false;
ans += i;
}
else if(!vis[j]) break;
}
}
}
printf("%lld\n",ans);
}
return 0;
}
CF1734D Slime Escape
题目分析:
我们显然可以考虑贪心。
从当前的位置到左边和到右边的贡献,但是如果直接选择走到贡献最优的虽然一定对但是会被卡掉,所以就考虑只走到第一个造成正贡献的位置。
然后对于左边和右边分别判断一下就好了,也有许多细节需要注意。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 4e5+5;
const int INF = 1e18+5;
int a[N];
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int t;
scanf("%lld",&t);
while(t--){
int n,k;
scanf("%lld%lld",&n,&k);
for(int i=1; i<=n; i++) scanf("%lld",&a[i]);
int l = k,r = k,v = a[k];
a[0] = a[n+1] = INF;
bool flag = true;
while(l != 0 && r != n + 1 && v >= 0){
int mn = INF,sum = 0,nl=l,nr=r;
for(int i = l-1; i>=0; i--){
sum += a[i];mn = min(mn,sum);
if(sum >= 0){
nl = i;
break;
}
}
if(nl != l){
if(mn + v >= 0){
v += sum;l=nl;
continue;
}
}
mn = INF,sum = 0;
for(int i=r+1; i<=n+1; i++){
sum += a[i];mn = min(mn,sum);
if(sum >= 0){
nr = i;
break;
}
}
if(nr != r){
if(v + mn >= 0){
v += sum;r=nr;
continue;
}
}
printf("NO\n");
flag = false;
break;
}
if(flag) printf("YES\n");
}
return 0;
}
CF1734E Rectangular Congruence
题目分析:
我们可以发现我们如果可以构造出一个符合要求的矩阵,那么对于其第 \(i\) 行整体减去 \(b[i] - a[i][i]\) 就可以变成符合题目要求的矩阵了。
那么就考虑如何构造一个符合要求的矩阵了。
下面一种构造方式: \(a[i][j] = i \times j\)
考虑证明也就是将原题中的式子移项之后就显然了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 400;
int a[N][N],b[N],n;
int mod(int x){
return ((x % n) + n) % n;
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%lld",&n);
for(int i=1; i<=n; i++) scanf("%lld",&b[i]);
for(int i=1; i<=n; i++){
for(int j=1; j<=n; j++){
a[i][j] = mod(i * j);
}
}
for(int i=1; i<=n; i++){
int tmp = mod(b[i] - a[i][i]);
for(int j=1; j<=n; j++){
a[i][j] = mod(a[i][j] + tmp);
}
}
for(int i=1; i<=n; i++){
for(int j=1; j<=n; j++){
printf("%lld ",a[i][j]);
}
printf("\n");
}
return 0;
}
9.26
【模板】后缀自动机
题目分析:
就是让出现次数乘以子串长度最大,要求出现至少两次。
一看到子串的信息当然想到后缀自动机,那么就直接维护每一个点的 \(endpos\) 的大小,然后如果大于等于 \(2\) 就直接乘以最长子串的长度累积到答案里就好了
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
const int ALP = 30;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N];
char s[N];
int last = 1,tot = 1,cnt,head[N],endpos[N],len[N],fa[N],ch[N][ALP];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void cop(int a,int b){
for(int i=0; i<ALP; i++){
ch[a][i] = ch[b][i];
}
len[a] = len[b];fa[a] = fa[b];
}
void extend(int c){
int p = last,np = last = ++tot;
endpos[tot] = 1;
len[np] = len[p] + 1;
for(;p && !ch[p][c]; p = fa[p]) ch[p][c] = np;
if(!p) fa[np] = 1;
else{
int q = ch[p][c];
if(len[q] == len[p] + 1) fa[np] = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[q] = fa[np] = nq;
for(;p && ch[p][c] == q; p = fa[p]) ch[p][c] = nq;
}
}
}
void dfs(int now){
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
dfs(to);
endpos[now] += endpos[to];
}
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%s",s+1);
int n = strlen(s+1);
for(int i=1; i<=n; i++) extend(s[i] - 'a');
for(int i=2; i<=tot; i++) add_edge(fa[i],i);
dfs(1);
long long ans = 0;
for(int i=2; i<=tot; i++){
if(endpos[i] > 1)
ans = max(ans,1ll * endpos[i] * len[i]);
}
printf("%lld\n",ans);
return 0;
}
[JSOI2012]玄武密码
题目分析:
既然是涉及 \(s\) 的子串那么就考虑对于 \(s\) 构造一个后缀自动机。
既然是 \(p\) 的前缀,那么就可以理解为从第一个字符开始一点点加入匹配,能匹配到的最远的位置。
直接在后缀自动机上跑就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e7+5;
const int ALP = 5;
int tot=1,last=1,fa[N],len[N],ch[N][ALP];
char s[N];
int get(char c){
if(c == 'E') return 0;
if(c == 'S') return 1;
if(c == 'W') return 2;
if(c == 'N') return 3;
}
void cop(int a,int b){
fa[a] = fa[b];len[a] = len[b];
for(int i=0; i<ALP; i++) ch[a][i] = ch[b][i];
}
void extend(int c){
int p = last,np = last = ++tot;
len[np] = len[p] + 1;
for(;p && !ch[p][c]; p = fa[p]) ch[p][c] = np;
if(!p) fa[np] = 1;
else{
int q = ch[p][c];
if(len[q] == len[p] + 1) fa[np] = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[q] = fa[np] = nq;
for(;p && ch[p][c] == q; p = fa[p]) ch[p][c] = nq;
}
}
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n,m;
scanf("%d%d",&n,&m);
scanf("%s",s+1);
for(int i=1; i<=n; i++) extend(get(s[i]));
for(int i=1; i<=m; i++){
scanf("%s",s+1);
int p = strlen(s + 1);
int now = 1;
int ans = 0;
for(int j = 1; j <= p; j++){
if(ch[now][get(s[j])])
now = ch[now][get(s[j])],ans = j;
else break;
}
printf("%d\n",ans);
}
return 0;
}
[JSOI2007]字符加密
题目分析:
显然就是一个循环同构的问题,所以就直接倍长一下那么每一个长度为 \(n\) 的子串就是我们的符合条件的子串之一。
那么就直接求一下所有后缀的排序就好了。因为这个也可以理解为只要前 \(n\) 个,后面几个都忽略,因为只要前 \(n\) 个显然就可以排好序了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2e6+6;
int n,M = 256,tax[MAXN],rk[MAXN],tp[MAXN],sa[MAXN],height[MAXN];
char s[MAXN];
//M 也就是可以理解为排名的个数,一开始可以是字符集大小
void get_sort(){
for(int i=0; i<=M; i++) tax[i] = 0;
for(int i=1; i<=n; i++) tax[rk[i]]++;
for(int i=1; i<=M; i++) tax[i] += tax[i-1];
for(int i=n; i>=1; i--) sa[tax[rk[tp[i]]]--] = tp[i];
}
void get_sa(){
for(int i=1; i<=n; i++) rk[i] = s[i],tp[i] = i;
get_sort();
for(int w=1,p=0; p<n; w<<=1,M = p){
p = 0;
for(int i=1; i<=w; i++) tp[++p] = n-w+i;
for(int i=1; i<=n; i++) if(sa[i] > w) tp[++p] = sa[i] - w;
get_sort();
swap(tp,rk);
rk[sa[1]] = p = 1;
for(int i=2; i<=n; i++) rk[sa[i]] = (tp[sa[i]] == tp[sa[i-1]] && tp[sa[i] + w] == tp[sa[i-1] + w] ? p : ++p);
}
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%s",s+1);n = strlen(s+1);
for(int i = n+1; i<=2*n; i++) s[i] = s[i-n];
n<<=1;
get_sa();
for(int i=1; i<=n; i++){
if(sa[i] <= n/2){
cout<<s[sa[i] + (n/2) - 1];
}
}
return 0;
}
SP705 SUBST1 - New Distinct Substrings
题目分析:
我们后缀自动机里每个节点代表的子串都一定没有交集。
所以求出每一个节点代表的子串的个数然后全部累计起来就好了。
子串的个数显然也就是 \(len[i] - len[fa[i]]\)
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
const int ALP = 26;
int last,tot,ch[N][ALP],fa[N],len[N];
char s[N];
void init(){
for(int i=1; i<=tot; i++){
fa[i] = 0;len[i] = 0;
for(int j=0; j<ALP; j++) ch[i][j] = 0;
}
last = tot = 1;
}
void cop(int a,int b){
fa[a] = fa[b];len[a] = len[b];
for(int i=0; i<ALP; i++) ch[a][i] = ch[b][i];
}
void extend(int c){
int p = last,np = last = ++tot;
len[np] = len[p] + 1;
for(;p && !ch[p][c]; p = fa[p]) ch[p][c] = np;
if(!p) fa[np] = 1;
else{
int q = ch[p][c];
if(len[q] == len[p] + 1) fa[np] = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[q] = fa[np] = nq;
for(;p && ch[p][c] == q; p = fa[p]) ch[p][c] = nq;
}
}
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int t;
scanf("%d",&t);
while(t--){
scanf("%s",s+1);
int n = strlen(s+1);
init();
for(int i=1; i<=n; i++) extend(s[i] - 'a');
int ans = 0;
for(int i=2; i<=tot; i++) ans += len[i] - len[fa[i]];
printf("%d\n",ans);
}
return 0;
}
SP8222 NSUBSTR - Substrings
题目分析:
因为是所有的子串所以考虑后缀自动机。
我们长度为 \(x\) 的子串的每一次出现都必然伴随着长度为 \([1,x-1]\) 的子串的出现,所以只需要对于每个点的长度最长的子串的长度打个标记。
然后最后倒叙累计一下就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5+5;
const int ALP = 26;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N];
int cnt,tot=1,last=1,head[N],len[N],fa[N],ch[N][ALP],endpos[N],tag[N];
char s[N];
void cop(int a,int b){
len[a] = len[b];fa[a] = fa[b];
for(int i=0; i<ALP; i++) ch[a][i] = ch[b][i];
}
void extend(int c){
int p = last,np = last = ++tot;
len[np] = len[p] + 1;endpos[tot] = 1;
for(;p && !ch[p][c]; p = fa[p]) ch[p][c] = np;
if(!p) fa[np] = 1;
else{
int q = ch[p][c];
if(len[q] == len[p] + 1) fa[np] = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[q] = fa[np] = nq;
for(;p && ch[p][c] == q;p = fa[p]) ch[p][c] = nq;
}
}
}
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void dfs(int now){
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
dfs(to);
endpos[now] += endpos[to];
}
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%s",s+1);
int n = strlen(s+1);
for(int i=1; i<=n; i++) extend(s[i] - 'a');
for(int i=2; i<=tot; i++) add_edge(fa[i],i);
dfs(1);
for(int i=2; i<=tot; i++) tag[len[i]] = max(tag[len[i]],endpos[i]);
for(int i = n; i>=1; i--) tag[i] = max(tag[i],tag[i + 1]);
for(int i=1; i<=n; i++) printf("%d\n",tag[i]);
return 0;
}
[TJOI2017]DNA
题目分析:
既然是 \(S_0\) 的所有子串那么就考虑对于 \(S_0\) 建后缀自动机。
其实就是让 \(S\) 在这个后缀自动机上匹配,看能匹配多少个位置的字符串。
但是我们还有可以修改三次的机会,那么就直接考虑 dfs,过程中记录一下修改了几个点就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5+5;
const int ALP = 4;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N];
int tot = 1,last = 1,cnt,ans,n,head[N],endpos[N],ch[N][ALP],len[N],fa[N];
char s[N];
int get(char c){
if(c == 'A') return 0;
if(c == 'T') return 1;
if(c == 'C') return 2;
if(c == 'G') return 3;
}
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void cop(int a,int b){
fa[a] = fa[b];len[a] = len[b];
for(int i=0; i<ALP; i++) ch[a][i] = ch[b][i];
}
void extend(int c){
int p = last,np = last = ++tot;
len[np] = len[p] + 1;endpos[tot] = 1;
for(;p && !ch[p][c]; p = fa[p]) ch[p][c] = np;
if(!p) fa[np] = 1;
else{
int q = ch[p][c];
if(len[q] == len[p] + 1) fa[np] = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[np] = fa[q] = nq;
for(;p && ch[p][c] == q; p = fa[p]) ch[p][c] = nq;
}
}
}
void init(){
tot = last = 1;cnt= 0;
memset(head,0,sizeof(head));
memset(ch,0,sizeof(ch));memset(len,0,sizeof(len));memset(endpos,0,sizeof(endpos));memset(fa,0,sizeof(fa));
}
void dfs(int now){
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
dfs(to);
endpos[now] += endpos[to];
}
}
void dfs(int now,int pos,int cost){
if(!now) return;
if(pos > n){ //所有出现位置都可以
ans += endpos[now];
endpos[now] = 0;
return;
}
for(int i=0; i<ALP; i++){
if(!ch[now][i]) continue;
if(get(s[pos]) == i) dfs(ch[now][i],pos+1,cost);
else if(cost < 3) dfs(ch[now][i],pos+1,cost+1);
}
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int t;
scanf("%lld",&t);
while(t--){
init();
scanf("%s",s+1);
n = strlen(s+1);
for(int i=1; i<=n; i++) extend(get(s[i]));
for(int i=2; i<=tot; i++) add_edge(fa[i],i);
scanf("%s",s+1);
n = strlen(s+1);
ans = 0;
dfs(1);
dfs(1,1,0);
printf("%lld\n",ans);
}
return 0;
}
[SDOI2016]生成魔咒
题目分析:
我们会发现只有我们新加入的点会对我们的答案产生影响。
因为其他的点虽然也有变化但是经过分裂以及父节点的变化也就可以将影响抵消掉。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5+5;
int len[N],fa[N],last=1,tot=1;
map<int,int> ch[N];
void cop(int a,int b){
len[a] = len[b];fa[a] = fa[b];
ch[a] = ch[b];
}
void extend(int c){
int p = last,np = last = ++tot;
len[np] = len[p] + 1;
for(;p && !ch[p][c]; p = fa[p]) ch[p][c] = np;
if(!p) fa[np] = 1;
else{
int q = ch[p][c];
if(len[q] == len[p] + 1) fa[np] = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[q] = fa[np] = nq;
for(;p && ch[p][c] == q; p = fa[p]) ch[p][c] = nq;
}
}
}
signed main(){
int n;
scanf("%lld",&n);
int ans = 0;
for(int i=1; i<=n; i++){
int c;
scanf("%lld",&c);
extend(c);
ans += len[last] - len[fa[last]];
printf("%lld\n",ans);
}
return 0;
}
9.27
SP1812 LCS2 - Longest Common Substring II
题目分析:
题目意思就是让我们求解 \(n\) 个串的最长公共子串,我们先从两个串的小问题开始考虑。
最暴力的办法就是对一个串建后缀自动机另一个串枚举每一个起点然后在后缀自动机上跑。
但是我们会发现这样的复杂度有点劣,那么考虑怎么优化。
我们其实发现不用枚举所有的起点然后在后缀自动机上跑,因为假设我们字符串 \(str(l,r)\) 跑到 \(p\) 停止了,那么有可能在以后的 \(str(l+1,r),str(l+2,r),\cdots\) 都是这样的,那么从什么开始不这样的了呢?
跳到 \(fa[p]\) 对应的最长串开始就不会这样了。
因为对于前面的几种情况其对应的后缀自动机上的状态不变所以一直都是在同一个位置卡住,而我们直接跳走也就不会了,此处和 \(KMP\) 就很相似了。
那么考虑多个串那么就每个串分别跑一边,然后记录 \(ans[i]\) 表示这 \(n\) 个串在 \(i\) 这个状态代表的字符串里最长的公共子串的长度。
但是我们需要注意对于每一个串我们跑出来的一个值 \(val[i]\) 也就是这个串在 \(i\) 这个状态的最长公共子串,需要去更新 \(val[fa[i]]\),因为假设我们 \(i\) 点存在长度为 \(p\) 的子串,那么对于 \(fa[i]\) 点肯定也是存在的,只不过需要注意需要与最长长度取 \(\min\)。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5+5;
const int ALP = 30;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N];
char s[N];
int cnt,tot=1,last=1,head[N],fa[N],len[N],ans[N],val[N],ch[N][ALP];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void dfs(int now){
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
dfs(to);
val[now] = max(val[now],val[to]);
}
}
void cop(int a,int b){
fa[a] = fa[b];len[a] =b;
for(int i=0; i<ALP; i++) ch[a][i] = ch[b][i];
}
void extend(int c){
int p = last,np = last = ++tot;
len[np] = len[p] + 1;
for(;p && !ch[p][c]; p = fa[p]) ch[p][c] = np;
if(!p) fa[np] = 1;
else{
int q = ch[p][c];
if(len[q] == len[p] + 1) fa[np] = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[q] = fa[np] = nq;
for(;p && ch[p][c] == q;p = fa[p]) ch[p][c] = nq;
}
}
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n;
scanf("%s",s+1);
for(int i=1; s[i]; i++) extend(s[i] - 'a');
for(int i=2; i<=tot; i++) add_edge(fa[i],i);
for(int i=1; i<=tot; i++) ans[i] = len[i];
while(~scanf("%s",s + 1)){
memset(val,0,sizeof(val));
int p = 1,l = 0;
int h = strlen(s + 1);
for(int j=1; j <= h; j++){
while(p && !ch[p][s[j] - 'a']) p = fa[p],l=len[p];
if(!p) p = 1,l = 0;
else{
p = ch[p][s[j] - 'a'];
l++;
}
val[p] = max(val[p],l);
}
dfs(1);
for(int j=2; j<=tot; j++) ans[j] = min(ans[j],val[j]);
}
int res = 0;
for(int i=2; i<=tot; i++) res = max(res,ans[i]);
printf("%d\n",res);
return 0;
}
[TJOI2015]弦论
题目分析:
我们其实就是要求解从每个节点出发沿着转移边走能走到的字符串的数量,然后根据这个值就可以求出来我们的第 \(k\) 小子串了。
对于 \(T = 0\) 也就是每个点的初值为 \(1\),因为不同位置只算一个。
对于 \(T = 0\) 也就是每个点的初值为 \(|endpos_i|\),因为不同位置算不同的。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+5;
const int ALP = 30;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N];
int tot = 1,last = 1,t,k,cnt,head[N],len[N],fa[N],ch[N][ALP],sz[N],endpos[N];
char s[N];
bool vis[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void cop(int a,int b){
len[a] = len[b];fa[a] = fa[b];
for(int i=0; i<ALP; i++) ch[a][i] = ch[b][i];
}
void extend(int c){
int p = last,np = last = ++tot;
endpos[tot] = 1;
len[np] = len[p] + 1;
for(;p && !ch[p][c]; p = fa[p]) ch[p][c] = np;
if(!p) fa[np] = 1;
else{
int q = ch[p][c];
if(len[q] == len[p] + 1) fa[np] = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[q] = fa[np] = nq;
for(;p && ch[p][c] == q; p = fa[p]) ch[p][c] = nq;
}
}
}
void get_endpos(int now){
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
get_endpos(to);
endpos[now] += endpos[to];
}
}
void get_sz(int now){
if(vis[now]) return;
vis[now] = true;
for(int i=0; i<ALP; i++){
if(!ch[now][i]) continue;
get_sz(ch[now][i]);
sz[now] += sz[ch[now][i]];
}
}
void get_out(int now,int k){
if(k <= endpos[now]) return;
k -= endpos[now];
for(int i = 0; i < ALP; i++){
if(!ch[now][i]) continue;
if(k > sz[ch[now][i]]) k -= sz[ch[now][i]];
else{
putchar('a' + i);get_out(ch[now][i],k);
break;
}
}
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%s",s+1);
scanf("%d%d",&t,&k);
int n = strlen(s+1);
for(int i=1; i<=n; i++) extend(s[i] - 'a');
for(int i=2; i<=tot; i++) add_edge(fa[i],i);
get_endpos(1);
for(int i=1; i<=tot; i++){
if(t == 0) sz[i] = endpos[i] = 1;
else sz[i] = endpos[i];
}
sz[1] = endpos[1] = 0;
get_sz(1);
if(sz[1] < k) printf("-1\n");
else get_out(1,k);
return 0;
}
[AHOI2013]差异
题目分析:
我们可以考虑把式子拆开计算。
对于后面这一坨也就是让我们求任意两个后缀的 \(lcp\) 的长度和。
我们可以发现一个性质,当 $i \not= j $ 时,\(i,j\) 这两个前缀的 \(lcs\) 就是对应在后缀自动机的 \(parent\) 树上的 \(lca\)。
所以就对原串的反串建一个后缀自动机就好了。
那么问题就可以转化为对于每一个点求有多少个点对的 \(lca\) 是他,直接 \(dfs\) 一遍就可以得出结果了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6+5;
const int ALP = 30;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N * 2];
int ans=0,last=1,tot=1,cnt,head[N],fa[N],ch[N][ALP],len[N],sz[N];
char s[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void cop(int a,int b){
fa[a] = fa[b];len[a] = len[b];
for(int i=0; i<ALP; i++) ch[a][i] = ch[b][i];
}
void extend(int c){
int p = last,np = last = ++tot;sz[tot] = 1;
len[np] = len[p] + 1;
for(;p && !ch[p][c]; p = fa[p]) ch[p][c] = np;
if(!p) fa[np] = 1;
else{
int q = ch[p][c];
if(len[q] == len[p] + 1) fa[np] = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[q] = fa[np] = nq;
for(; p && ch[p][c] == q; p = fa[p]) ch[p][c] = nq;
}
}
}
void dfs(int now){
int tmp = 0;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
dfs(to);
tmp += sz[now] * sz[to];
sz[now] += sz[to];
}
ans += tmp * len[now] * 2;
}
signed main(){
scanf("%s",s+1);
int n = strlen(s+1);
for(int i=n; i>=1; i--) extend(s[i] - 'a');
for(int i=2; i<=tot; i++) add_edge(fa[i],i);
dfs(1);
printf("%lld\n",n * (n + 1) * (n - 1) / 2 - ans);
return 0;
}
[HAOI2016]找相同字符
题目分析:
看到子串自然可以想到后缀自动机。
既然是和两个字符串的所有子串都有关系,那么就直接对两个字符串都建后缀自动机。
然后从起始点开始沿着相同的转移边可以走到的点也就是可以相互匹配的点。
显然这里的匹配满足乘法原理,所以直接乘起来就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 4e5+5;
const int ALP = 30;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
};
struct SAM{
edge e[N * 2];
int last=1,tot=1,cnt,head[N],endpos[N],len[N],fa[N],ch[N][ALP];
void cop(int a,int b){
len[a] = len[b];fa[a] = fa[b];
for(int i=0; i<ALP; i++) ch[a][i] = ch[b][i];
}
void extend(int c){
int p = last,np = last = ++tot;endpos[tot] = 1;
len[np] = len[p] + 1;
for(;p && !ch[p][c]; p = fa[p]) ch[p][c] = np;
if(!p) fa[np] = 1;
else{
int q = ch[p][c];
if(len[q] == len[p] + 1) fa[np] = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[q] = fa[np] = nq;
for(;p && ch[p][c] == q; p = fa[p]) ch[p][c] = nq;
}
}
}
void dfs(int now){
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
dfs(to);
endpos[now] += endpos[to];
}
}
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void build_edge(){
for(int i=2; i<=tot; i++) add_edge(fa[i],i);
}
void init(){
build_edge();
dfs(1);
}
}sam1,sam2;
int ans = 0;
char s[N];
void dfs(int x,int y){
if(x != 1 && y != 1) ans += sam1.endpos[x] * sam2.endpos[y];
for(int i = 0; i<ALP; i++){
if(sam1.ch[x][i] && sam2.ch[y][i]){
dfs(sam1.ch[x][i],sam2.ch[y][i]);
}
}
}
signed main(){
scanf("%s",s+1);
int n = strlen(s+1);
for(int i=1; i<=n; i++) sam1.extend(s[i] - 'a');
sam1.init();
scanf("%s",s+1);
n = strlen(s+1);
for(int i=1; i<=n; i++) sam2.extend(s[i] - 'a');
sam2.init();
dfs(1,1);
printf("%lld\n",ans);
return 0;
}
[NOI2015] 品酒大会
题目分析:
我们会发现所谓 \(p,q\) 为 \(r\) 相似,就意味着 \(p,q\) 这两个后缀的 \(lcp\) 的长度至少为 \(r\)。
也就可以显然地建反串的后缀自动机,转化为 \(p,q\) 两个前缀的 \(lcs\) 的长度至少为 \(r\)。
观察这两问。
第一问就是 [AHOI2013]差异,直接做就好了。
对于第二问也就是类似这个题的做法,\(dp\) 记录一下子树内的最大值最小值次大值次小值就好了。
记录次大值和次小值是因为可能存在负数乘负数,结果为正。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 6e5+5;
const int ALP = 30;
const int INF = 1e18+5;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N];
int tot=1,last=1,cnt,mx[N][2],mn[N][2],head[N],ans[N],res[N],sz[N],fa[N],len[N],ch[N][ALP],val[N],a[N];
char s[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void cop(int a,int b){
fa[a] = fa[b];len[a] = len[b];
memcpy(ch[a],ch[b],sizeof(ch[b]));
}
void extend(int c,int pos){
int p = last,np = last = ++tot;sz[tot] = 1;val[tot] = a[pos];
mx[tot][0] = mn[tot][0] = val[tot];
len[np] = len[p] + 1;
for(;p && !ch[p][c]; p = fa[p]) ch[p][c] = np;
if(!p) fa[np] = 1;
else{
int q = ch[p][c];
if(len[q] == len[p] + 1) fa[np] = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[q] = fa[np] = nq;
for(;p && ch[p][c] == q; p = fa[p]) ch[p][c] = nq;
}
}
}
void dfs(int now){
int tmp = 0;
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
dfs(to);
tmp += sz[now] * sz[to];
sz[now] += sz[to];
if(mx[now][1] < mx[to][1]) mx[now][1] = mx[to][1];
if(mx[now][1] > mx[now][0]) swap(mx[now][0],mx[now][1]);
if(mx[now][1] < mx[to][0]) mx[now][1] = mx[to][0];
if(mx[now][1] > mx[now][0]) swap(mx[now][0],mx[now][1]);
if(mn[now][1] > mn[to][1]) mn[now][1] = mn[to][1];
if(mn[now][1] < mn[now][0]) swap(mn[now][0],mn[now][1]);
if(mn[now][1] > mn[to][0]) mn[now][1] = mn[to][0];
if(mn[now][1] < mn[now][0]) swap(mn[now][0],mn[now][1]);
}
ans[len[now]] += tmp;
if(mx[now][0] != -INF && mx[now][1] != -INF)
res[len[now]] = max(res[len[now]],max(mx[now][0] * mx[now][1],mn[now][0] * mn[now][1]));
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n;
scanf("%lld",&n);
scanf("%s",s+1);
for(int i=1; i<=n; i++) scanf("%lld",&a[i]);
for(int i=1; i<=2 * n; i++){
mx[i][0] = mx[i][1] = -INF;
mn[i][0] = mn[i][1] = INF;
res[i] = -INF;
}
for(int i=n; i>=1; i--) extend(s[i] - 'a',i);
for(int i=2; i<=tot; i++) add_edge(fa[i],i);
dfs(1);
for(int i=n; i>=0; i--) ans[i] += ans[i+1];
for(int i=n; i>=0; i--) res[i] = max(res[i],res[i+1]);
for(int i=0; i<n; i++){
printf("%lld %lld\n",ans[i],res[i] == -INF ? 0 : res[i]);
}
return 0;
}
[NOI2016] 优秀的拆分
题目分析;
我们可以维护 \(a[i]\) 代表 \([1,i]\) 中 \(AA\) 串的数量,维护 \(b[i]\) 代表 \([i,n]\) 中 \(AA\) 串的数量。
然后考虑直接枚举 \(|A|\),假设为 \(len\),然后每 \(len\) 插一个点。
对于相邻的两个点如果他们的前缀的 \(lcs\) 和 他们后缀的 \(lcp\) 的和大于等于 \(len\) 则代表可以形成合法的 \(AA\) 串。
区间加一下就好了。
画个图更加直观,之后有空再补了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 6e4+5;
const int ALP = 27;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
};
int n,a[N],b[N];
char s[N];
struct SAM{
edge e[2 * N];
int tot=1,last=1,cnt,dep[N],head[N],tag[N],fa[N][20],len[N],ch[N][ALP];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void cop(int a,int b){
memcpy(ch[a],ch[b],sizeof(ch[b]));
memcpy(fa[a],fa[b],sizeof(fa[b]));
len[a] = len[b];
}
void extend(int c,int pos){
int p = last,np = last = ++tot;tag[pos] = tot;
len[np] = len[p] + 1;
for(;p && !ch[p][c]; p = fa[p][0]) ch[p][c] = np;
if(!p) fa[np][0] = 1;
else{
int q = ch[p][c];
if(len[q] == len[p] + 1) fa[np][0] = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[q][0] = fa[np][0] = nq;
for(; p && ch[p][c] == q; p = fa[p][0]) ch[p][c] = nq;
}
}
}
void dfs(int now){
for(int i = head[now]; i; i = e[i].nxt){
int to = e[i].to;
dep[to] = dep[now] + 1;
dfs(to);
}
}
void init(){
tag[0] = 1;
for(int i=1; i<=17; i++){
for(int j=1; j<=tot; j++){
fa[j][i] = fa[fa[j][i-1]][i-1];
}
}
for(int i=2; i<=tot; i++) add_edge(fa[i][0],i);
dep[1] = 1;
dfs(1);
}
int query(int x,int y){
x = tag[x],y = tag[y];
if(dep[x] < dep[y]) swap(x,y);
for(int i = 17; i>=0; i--){
if(dep[fa[x][i]] >= dep[y]){
x = fa[x][i];
}
}
if(x == y) return len[x];
for(int i = 17; i>=0; i--){
if(fa[x][i] != fa[y][i]){
x = fa[x][i];
y = fa[y][i];
}
}
return len[fa[x][0]];
}
void clear(){
tot = last = 1;cnt = 0;
len[1] = 0;
memset(fa,0,sizeof(fa));
memset(ch,0,sizeof(ch));
memset(head,0,sizeof(head));
}
}sam1,sam2;
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int ti;
scanf("%lld",&ti);
while(ti--){
scanf("%s",s+1);
n = strlen(s+1);
for(int i=1; i<=n; i++) sam1.extend(s[i] - 'a',i);
for(int i=n; i>=1; i--) sam2.extend(s[i] - 'a',i);
sam1.init();sam2.init();
for(int len=1; len<=n/2; len++){
for(int l=len; l + len - 1<=n; l+=len){
int r = l + len;
int L = l-1,R = r-1;
int lcp = 0,lcs = 0;
if(r <= n) lcp = sam2.query(l,r),lcp = min(lcp,len);
if(L >= 1) lcs = sam1.query(L,R),lcs = min(lcs,len-1);
if(lcs + lcp >= len){
b[l - lcs]++;b[l - lcs + (lcp + lcs - len + 1)]--;
a[r + lcp - (lcp + lcs - len + 1)]++;a[r + lcp]--;
}
}
}
for(int i=1; i<=n; i++){
a[i] += a[i-1];
b[i] += b[i-1];
}
int ans = 0;
for(int i=2; i<=n; i++) ans += a[i-1] * b[i];
printf("%lld\n",ans);
sam1.clear();sam2.clear();
for(int i=0; i<=n+1; i++){
a[i] = b[i] = 0;
}
}
return 0;
}
9.28
[USACO17DEC]Standing Out from the Herd P
题目分析:
我们考虑直接建一个广义后缀自动机,对于每一个节点维护它是那个字符串的子串,如果是多个就直接置为 \(-1\) 表示不合法。
然后最后扫一遍统计一下答案就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5+5;
const int ALP = 30;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N * 2];
int tot=1,last=1,cnt,head[N],ans[N],fa[N],len[N],flag[N],ch[N][ALP];
char s[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void cop(int a,int b){
memcpy(ch[a],ch[b],sizeof(ch[b]));
fa[a] = fa[b];len[a] = len[b];
}
void extend(int c,int pos){
if(ch[last][c]){
int p = last,q = ch[last][c];
if(len[q] == len[p] + 1){
flag[q] = -1;
last = q;
}
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[q] = nq;flag[nq] = pos;
for(;p && ch[p][c] == q; p = fa[p]) ch[p][c] = nq;
last = nq;
}
return;
}
int p = last,np = last = ++tot;
len[np] = len[p] + 1;flag[tot] = pos;
for(;p && !ch[p][c]; p = fa[p]) ch[p][c] = np;
if(!p) fa[np] = 1;
else{
int q = ch[p][c];
if(len[q] == len[p] + 1) fa[np] = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[q] = fa[np] = nq;
for(;p && ch[p][c] == q; p = fa[p]) ch[p][c] = nq;
}
}
}
void dfs(int now){
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
dfs(to);
if(flag[now] == -1) continue;
if(!flag[now]) flag[now] = flag[to];
else if(flag[now] != flag[to]) flag[now] = -1;
}
if(flag[now] != -1)
ans[flag[now]] += len[now] - len[fa[now]];
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int ti;
scanf("%lld",&ti);
for(int i=1; i<=ti; i++){
scanf("%s",s+1);
int n = strlen(s+1);
last = 1;
for(int j=1; j<=n; j++) extend(s[j] - 'a',i);
}
for(int i=2; i<=tot; i++) add_edge(fa[i],i);
dfs(1);
for(int i=1; i<=ti; i++)
printf("%lld\n",ans[i]);
return 0;
}
[ZJOI2015]诸神眷顾的幻想乡
题目分析:
这题好像是和本质不同的子串有一些关系。
我们会观察到叶子节点数量很少,所以就肯定和叶子节点有关系。
我们可以观察到一点:任意两个点之间的路径一定可以表示为某两个叶子的路径的一部分。
所以直接枚举哪一个叶子当作根节点,然后扫一遍整棵树将扫到的顺路插入广义后缀自动机里,最后求一个本质不同的子串数量就好了。
(广义后缀自动机竟然支持在任意节点后再新加入一个字符,也就是与之前插入的串再次形成一个字符串)
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e6+5;
const int ALP = 11;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[2 * N];
int tot=1,last=1,cnt,deg[N],head[N],len[N],ch[N][ALP],fa[N],s[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void cop(int a,int b){
memcpy(ch[a],ch[b],sizeof(ch[b]));
fa[a] = fa[b];len[a] = len[b];
}
int extend(int c,int pos){
if(ch[pos][c]){
int p = pos,q = ch[pos][c];
if(len[q] == len[p] + 1) last = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[q] = nq;
for(;p && ch[p][c] == q; p = fa[p]) ch[p][c] = nq;
last = nq;
}
}
else{
int p = pos,np = last = ++tot;
len[np] = len[p] + 1;
for(;p && !ch[p][c]; p = fa[p]) ch[p][c] = np;
if(!p) fa[np] = 1;
else{
int q = ch[p][c];
if(len[q] == len[p] + 1) fa[np] = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[np] = fa[q] = nq;
for(;p && ch[p][c] == q; p = fa[p]) ch[p][c] = nq;
}
}
last = np;
}
return last;
}
void dfs(int now,int fath,int last){
int ls = extend(s[now],last);
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
dfs(to,now,ls);
}
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n,c;
scanf("%lld%lld",&n,&c);
for(int i=1; i<=n; i++) scanf("%lld",&s[i]);
for(int i=1; i<n; i++){
int u,v;
scanf("%lld%lld",&u,&v);
add_edge(u,v);add_edge(v,u);
deg[u]++;deg[v]++;
}
for(int i=1; i<=n; i++){
if(deg[i] == 1)
dfs(i,0,1);
}
int ans = 0;
for(int i=2; i<=tot; i++) ans += len[i] - len[fa[i]];
printf("%lld\n",ans);
return 0;
}
SPOJ JZPGYZ - Sevenk Love Oimaster
题目分析:
我们可以考虑对于 \(n\) 个模式串建立广义后缀自动机。
那么问题就是如何求的每一个查询串是多少个串的子串,其实也就是这个串对应的后缀自动机上的节点是多少个模式串的子串。
我们可以对于每一个节点维护一棵线段树,表示这个节点对应的模式串。
因为如果状态 \(S\) 中的子串是模式串 \(T\) 的子串,那么 \(fa[S]\) 对应的子串也一定是模式串 \(T\) 的子串,那么直接从底向上线段树合并就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
const int ALP = 30;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N * 2];
int tot=1,last=1,n,m,res,cnt,head[N],rt[N],len[N],fa[N],ch[N][ALP],lson[4 * N],rson[4 * N],sum[4 * N];
char s[N];
void pushup(int now){
sum[now] = sum[lson[now]] + sum[rson[now]];
}
int newnode(int lst){
++res;
sum[res] = sum[lst];lson[res] = lson[lst];rson[res] = rson[lst];
return res;
}
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void change(int &root,int now_l,int now_r,int pos,int val){
if(!root) root = ++res;
if(now_l == now_r){
sum[root] = val;
return;
}
int mid = (now_l + now_r)>>1;
if(pos <= mid) change(lson[root],now_l,mid,pos,val);
else change(rson[root],mid+1,now_r,pos,val);
pushup(root);
}
void merge(int &root1,int &root2,int now_l,int now_r){
if(!root1){
root1 = newnode(root2);
return;
}
if(now_l == now_r){
sum[root1] |= sum[root2];
return;
}
int mid = (now_l + now_r)>>1;
if(!lson[root1]) lson[root1] = newnode(lson[root2]);
else merge(lson[root1],lson[root2],now_l,mid);
if(!rson[root1]) rson[root1] = newnode(rson[root2]);
else merge(rson[root1],rson[root2],mid+1,now_r);
pushup(root1);
}
void cop(int a,int b){
memcpy(ch[a],ch[b],sizeof(ch[b]));
len[a] = len[b];fa[a] = fa[b];
}
void extend(int c,int num){
if(ch[last][c]){
int p = last,q = ch[last][c];
if(len[q] == len[p] + 1) last = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[q] = nq;
for(;p && ch[p][c] == q; p = fa[p]) ch[p][c] = nq;
last = nq;
}
}
else{
int p = last,np = last = ++tot;
len[np] = len[p] + 1;
for(; p && !ch[p][c]; p = fa[p]) ch[p][c] = np;
if(!p) fa[np] = 1;
else{
int q = ch[p][c];
if(len[q] == len[p] + 1) fa[np] = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[np] = fa[q] = nq;
for(;p && ch[p][c] == q; p = fa[p]) ch[p][c] = nq;
}
}
}
change(rt[last],1,n,num,1);
}
void dfs(int now){
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
dfs(to);
merge(rt[now],rt[to],1,n);
}
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++){
scanf("%s",s+1);
int h = strlen(s+1);
last = 1;
for(int j=1; j<=h; j++) extend(s[j] - 'a',i);
}
for(int i=2; i<=tot; i++) add_edge(fa[i],i);
dfs(1); //线段树合并
for(int i=1; i<=m; i++){
scanf("%s",s+1);
int h = strlen(s+1);
int ans = 0,now = 1;
for(int j=1; j<=h; j++){
if(ch[now][s[j] - 'a']) now = ch[now][s[j] - 'a'];
else{
now = -1;
break;
}
}
if(now) ans = sum[rt[now]];
printf("%d\n",ans);
}
return 0;
}
CF235C Cyclical Quest
题目分析:
看到循环同构自然可以想到倍长。
那么就将询问串倍长,那么询问就变成了询问多少询问串的子串在母串 \(S\) 中并且长度为其原长。
那么直接拿询问串在母串的后缀自动机上跑就好了。
需要注意一点:如果我们当前匹配的到的长度大于原长,那么我们就应该尝试再在前面删一些字符,也就是跳父亲,因为这样会使得答案更优,更符合条件。
我们也要注意每个状态只可以被计算一次就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
const int ALP = 27;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N];
int last=1,tot=1,cnt,endpos[N],head[N],ch[N][ALP],fa[N],len[N];
char s[N];
int vis[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void cop(int a,int b){
len[a] = len[b];fa[a] = fa[b];
memcpy(ch[a],ch[b],sizeof(ch[b]));
}
void extend(int c){
int p = last,np = last = ++tot;endpos[tot] = 1;
len[np] = len[p] + 1;
for(;p && !ch[p][c]; p = fa[p]) ch[p][c] = np;
if(!p) fa[np] = 1;
else{
int q = ch[p][c];
if(len[q] == len[p] + 1) fa[np] = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[q] = fa[np] = nq;
for(;p && ch[p][c] == q; p = fa[p]) ch[p][c] = nq;
}
}
}
void dfs(int now){
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
dfs(to);
endpos[now] += endpos[to];
}
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%s",s+1);
int n = strlen(s+1);
for(int i=1; i<=n; i++) extend(s[i] - 'a');
for(int i=2; i<=tot; i++) add_edge(fa[i],i);
dfs(1);
int t;
scanf("%d",&t);
int pos = 1;
while(t--){
scanf("%s",s+1);
int n = strlen(s+1);
for(int i = n+1; i<=2*n; i++) s[i] = s[i - n];
n <<= 1;
int now = 1,l = 0;
int res = 0;
for(int i=1; i<=n; i++){
while(now && !ch[now][s[i] - 'a']) now = fa[now],l = len[now];
if(!now){
now = 1;
l = 0;
}
else{
now = ch[now][s[i] - 'a'];
l++;
if(l >= (n / 2)){
while(len[fa[now]] >= n/2) now = fa[now];
if(vis[now] != pos){
res += endpos[now];
vis[now] = pos;
}
l = n/2;
}
}
}
printf("%d\n",res);
++pos;
}
return 0;
}
CF802I Fake News (hard)
题目分析:
我们直接建出后缀自动机。
对于每一个状态其代表的字符串的出现次数我们可以知道,其代表的字符串数量我们可以知道。
然后按照题目的式子乘一下就出来了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5+5;
const int ALP = 30;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N];
int last=1,tot=1,cnt,head[N],fa[N],len[N],ch[N][ALP],endpos[N];
char s[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void cop(int a,int b){
fa[a] = fa[b];len[a] = len[b];
memcpy(ch[a],ch[b],sizeof(ch[b]));
}
void extend(int c){
int p = last,np = last = ++tot;endpos[tot] = 1;
len[np] = len[p] + 1;
for(;p && !ch[p][c]; p = fa[p]) ch[p][c] = np;
if(!p) fa[np] = 1;
else{
int q = ch[p][c];
if(len[q] == len[p] + 1) fa[np] = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[q] = fa[np] = nq;
for(;p && ch[p][c] == q; p = fa[p]) ch[p][c] = nq;
}
}
}
void dfs(int now){
for(int i = head[now]; i; i = e[i].nxt){
int to = e[i].to;
dfs(to);
endpos[now] += endpos[to];
}
}
signed main(){
int t;
scanf("%lld",&t);
while(t--){
scanf("%s",s+1);
int n = strlen(s+1);
for(int i=1; i<=n; i++) extend(s[i] - 'a');
for(int i=2; i<=tot; i++) add_edge(fa[i],i);
dfs(1);
int ans = 0;
for(int i=2; i<=tot; i++) ans += endpos[i] * endpos[i] * (len[i] - len[fa[i]]);
printf("%lld\n",ans);
memset(endpos,0,sizeof(endpos));memset(fa,0,sizeof(fa));memset(len,0,sizeof(len));memset(ch,0,sizeof(ch));memset(head,0,sizeof(head));
last = tot = 1;cnt = 0;
}
return 0;
}
CF873F Forbidden Indices
题目分析:
题目的提示很明确:子串、结束位置。
所以显然建后缀自动机。
那么考虑如何维护结束位置这个限制:很好搞就在建后缀自动机的时候对于禁止的位置的 \(endpos\) 的初值设为 \(0\)。
这样也就意味着我们这个结束位置以后都是不可用的了。
最后求一遍 \(endpos\) 再与最长子串的长度乘一下再取个最大值就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 4e5+5;
const int ALP = 30;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N];
int tot=1,last=1,cnt,endpos[N],head[N],fa[N],len[N],ch[N][ALP];
char s[N],opt[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void cop(int a,int b){
fa[a] = fa[b];len[a] = len[b];
memcpy(ch[a],ch[b],sizeof(ch[b]));
}
void extend(int c,int op){
int p = last,np = last = ++tot;endpos[tot] = (op == 0);
len[np] = len[p] + 1;
for(;p && !ch[p][c]; p = fa[p]) ch[p][c] = np;
if(!p) fa[np] = 1;
else{
int q = ch[p][c];
if(len[q] == len[p] + 1) fa[np] = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[q] = fa[np] = nq;
for(;p && ch[p][c] == q; p = fa[p]) ch[p][c] = nq;
}
}
}
void dfs(int now){
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
dfs(to);
endpos[now] += endpos[to];
}
}
signed main(){
int n;
scanf("%lld",&n);
scanf("%s",s+1);
scanf("%s",opt+1);
for(int i=1; i<=n; i++) extend(s[i] - 'a',opt[i] - '0');
for(int i=2; i<=tot; i++) add_edge(fa[i],i);
dfs(1);
int ans = 0;
for(int i=2; i<=tot; i++) ans = max(ans,len[i] * endpos[i]);
printf("%lld\n",ans);
return 0;
}
[P5212][BZOJ2555] SubString
题目分析:
看上去就是后缀自动机的操作。
但是我们会发现对于询问:如果每一次都 \(dfs\) 一遍求解 \(endpos\) 然后再拿 \(s\) 去跑,一定会超时。
那么我们就考虑我们在后缀自动机的构建过程中,每次加入一个新点,其实对于 \(endpos\) 的修改就是相当于一条到根的路径的修改。
换句话说我们只要可以知道每一个点的子树内的点权和就可以知道这个点的 \(endpos\) 大小。
看到加边、删边自然可以想到 \(LCT\),直接使用 \(LCT\) 维护子树内权值和就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
const int ALP = 3;
namespace LCT{
int top,st[N],sz[N],lsz[N],ch[N][2],fa[N],val[N];
bool tag[N];
int getson(int x){
return ch[fa[x]][1] == x;
}
int isroot(int x){
return ch[fa[x]][getson(x)] != x;
}
void pushup(int now){
sz[now] = val[now] + sz[ch[now][0]] + sz[ch[now][1]] + lsz[now];
}
void reverse(int x){
swap(ch[x][1],ch[x][0]);
tag[x] ^= 1;
}
void pushdown(int now){
if(tag[now]){
if(ch[now][0]) reverse(ch[now][0]);
if(ch[now][1]) reverse(ch[now][1]);
tag[now] = false;
}
}
void rotate(int x){
int y = fa[x],z = fa[y],k = getson(x);
if(!isroot(y)) ch[z][getson(y)] = x;fa[x] = z;
ch[y][k] = ch[x][k^1]; if(ch[x][k^1]) fa[ch[x][k^1]] = y;
ch[x][k^1] = y;fa[y] = x;
pushup(y);pushup(x);pushup(z);
}
void splay(int x){
top = 1;st[top] = x;
for(int y = x; !isroot(y); y=fa[y]) st[++top] = fa[y];
for(int i=top; i; i--) pushdown(st[i]);
for(;!isroot(x);rotate(x))
if(!isroot(fa[x])) rotate(getson(x) == getson(fa[x]) ? fa[x] : x);
}
void access(int x){
for(int pre = 0;x;pre=x,x = fa[x]){
splay(x);
lsz[x] += sz[ch[x][1]] - sz[pre];
ch[x][1] = pre;
pushup(x);
}
}
void makeroot(int x){
access(x);splay(x);reverse(x);
}
int findroot(int x){
access(x);splay(x);
while(ch[x][0]) x = ch[x][0];
splay(x);
return x;
}
void link(int x,int y){
makeroot(x);
if(findroot(y) == x) return;
makeroot(y);
fa[x] = y;lsz[y] += sz[x];
pushup(y);
}
void cut(int x,int y){
makeroot(x);access(y);splay(y);
if(fa[x] == y && !ch[x][1]){
ch[y][0] = fa[x] = 0;
}
pushup(y);
}
int query(int x){
makeroot(1);access(x);splay(x);
return lsz[x] + val[x] + sz[ch[x][1]];
}
void init(int x){
sz[x] = val[x] = 1;
}
}
namespace SAM{
int last=1,tot=1,ch[N][ALP],fa[N],len[N];
void cop(int a,int b){
fa[a] = fa[b];len[a] = len[b];
memcpy(ch[a],ch[b],sizeof(ch[b]));
}
void extend(int c){
int p = last,np = last = ++tot;
len[np] = len[p] + 1;LCT::init(tot);
for(;p && !ch[p][c]; p = fa[p]) ch[p][c] = np;
if(!p) LCT::link(np,1),fa[np] = 1;
else{
int q = ch[p][c];
if(len[q] == len[p] + 1) LCT::link(np,q),fa[np] = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
LCT::cut(q,fa[q]);LCT::link(q,nq);LCT::link(np,nq);LCT::link(nq,fa[nq]);
fa[q] = fa[np] = nq;
for(;p && ch[p][c] == q; p = fa[p]) ch[p][c] = nq;
}
}
}
void Insert(string s){
for(int i=0; i<s.size(); i++)
extend(s[i] - 'A');
}
int query(string s){
int now = 1;
for(int i=0; i<s.size(); i++){
if(ch[now][s[i] - 'A'])
now = ch[now][s[i] - 'A'];
else return 0;
}
return LCT::query(now);
}
}
char chars[N];
string decodeWithMask(string s, int mask) {
int tot = 0;
for(int i=0; i<s.size(); i++) chars[tot++] = s[i];
for (int j = 0; j < tot; j++) {
mask = (mask * 131 + j) % tot;
char t = chars[j];
chars[j] = chars[mask];
chars[mask] = t;
}
string tmp = "";
for(int i=0; i<tot; i++){
tmp = tmp + chars[i];
}
return tmp;
// return s;
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int q;
string s;
scanf("%d",&q);
cin>>s;
SAM::Insert(s);
int mask = 0;
for(int i=1; i<=q; i++){
string opt;
cin>>opt>>s;
s = decodeWithMask(s,mask);
if(opt == "ADD") SAM::Insert(s);
else{
int ans = SAM::query(s);
printf("%d\n",ans);
mask ^= ans;
}
}
return 0;
}
CF1260D A Game with Traps
题目分析;
显然我们可以考虑直接二分我们选择的人的敏捷度的最小值是多少,然后考虑怎么 check。
显然最优的策略一定是带着小队来到不能走的陷阱前,我走过去拿掉,然后再走回来再带着小队前进。
小队前进的路径一定是 \(n+1\),我来回的路径也一定相等,所以就是考虑最优决策下我从所有的有妨碍的陷阱前走完所有陷阱的最小时间。
排一下序扫一遍就出来了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+5;
struct node{
int l,r,d;
}q[N];
int a[N],n,m,k,t;
pair<int,int> b[N];
bool check(int limit){
int tot = 0;
int cnt = 0;
for(int i=1; i<=k; i++){
if(q[i].d > limit)
b[++tot] = {q[i].l,q[i].r};
}
int now = 0;
sort(b+1,b+tot+1);
for(int i=1; i<=tot; i++){
if(b[i].first <= now){
cnt += max(0,b[i].second - now);
now = max(now,b[i].second);
}
else{
cnt += b[i].second - b[i].first + 1;
now = b[i].second;
}
}
cnt = cnt * 2 + n + 1;
return cnt <= t;
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%d%d%d%d",&m,&n,&k,&t);
for(int i=1; i<=m; i++) scanf("%d",&a[i]);
sort(a+1,a+m+1);
for(int i=1; i<=k; i++) scanf("%d%d%d",&q[i].l,&q[i].r,&q[i].d);
int l = 0,r = 1000000;
int ans = 0;
while(l <= r){
int mid = (l + r)>>1;
if(check(mid)){ //最小选择 mid
ans = mid;
r = mid - 1;
}
else l = mid + 1;
}
int res = 0;
for(int i=1; i<=m; i++){
if(a[i] >= ans)
res++;
}
printf("%d\n",res);
return 0;
}
CF1260E Tournament
题目分析:
我们可以发现:第一个人一定会选。
如果朋友的编号小于等于 \(n/2\) 那么还要多选一个,如果小于等于 \(n/4\) 要再次多选一次。
那么我们就直接维护一个最小值,每次选择一个最小值就好了。
而我们显然就是倒着扫一遍,如果扫到的是 \(2\) 的幂就选择。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1<<19;
int a[N];
signed main(){
int n;
scanf("%lld",&n);
for(int i=1; i<=n; i++) scanf("%lld",&a[i]);
priority_queue<int,vector<int>,greater<int> > q;
int ans = 0;
for(int i=n; i>=1; i--){
if(a[i] == -1) break;
q.push(a[i]);
if(!(i & (i-1))) ans += q.top(),q.pop();
}
printf("%lld\n",ans);
return 0;
}
9.29
[SDOI2008] Sandy 的卡片
题目分析:
看上去就很像一个最长公共子串。
所谓能全部加上一个数使得它们相同也就是说他们的变化量的对应关系是相等的。
也就是差分数组是一样的,也就是说在差分数组上跑一遍最长公共子串就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[N];
int last=1,tot=1,cnt,n,m,head[N],s[N],ans[N],len[N],fa[N],val[N];
map<int,int> ch[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void cop(int a,int b){
fa[a] = fa[b];len[a] = len[b];
ch[a] = ch[b];
}
void extend(int c){
int p = last,np = last = ++tot;
len[np] = len[p] + 1;
for(;p && !ch[p][c]; p = fa[p]) ch[p][c] = np;
if(!p) fa[np] = 1;
else{
int q = ch[p][c];
if(len[q] == len[p] + 1) fa[np] = q;
else{
int nq = ++tot;
cop(nq,q);len[nq] = len[p] + 1;
fa[q] = fa[np] = nq;
for(;p && ch[p][c] == q; p = fa[p]) ch[p][c] = nq;
}
}
}
void dfs(int now){
for(int i = head[now]; i;i = e[i].nxt){
int to = e[i].to;
dfs(to);
val[now] = max(val[now],val[to]);
}
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n;
scanf("%d",&n);
scanf("%d",&m);
for(int i=1; i<=m; i++) scanf("%d",&s[i]);
for(int i=m; i>=2; i--) s[i] -= s[i-1];
for(int i=2; i<=m; i++) extend(s[i]);
// for(int i=2; i<=m; i++) printf("%d ",s[i]);
// printf("\n");
for(int i=2; i<=tot; i++) add_edge(fa[i],i);
for(int i=2; i<=tot; i++) ans[i] = len[i];
for(int i=1; i<n; i++){
scanf("%d",&m);
for(int j=1; j<=m; j++) scanf("%d",&s[j]);
for(int j=m; j>=2; j--) s[j] -= s[j-1];
// for(int j=2; j<=m; j++) printf("%d ",s[j]);
// printf("\n");
int now = 1,l = 0;
for(int j=2; j<=m; j++){
while(now && !ch[now][s[j]]) now = fa[now],l = len[now];
if(!now){
now = 1,l = 0;
}
else{
now = ch[now][s[j]];
l++;
}
val[now] = max(val[now],l);
}
dfs(1);
for(int j=1; j<=tot; j++) ans[j] = min(ans[j],val[j]),val[j] = 0;
}
int res = 0;
for(int i=1; i<=tot; i++) res = max(res,ans[i]);
printf("%d\n",res + 1);
return 0;
}
CF241B Friends
题目分析:
看到这种 \(k\) 大异或显然想到建 01trie 树。
首先我们可以考虑二分出第 \(k\) 大是多少。
具体就是二分出一个数然后把每一个数放在 trie 树上跑一遍,求和后判断大于他的数的个数。
那么我们就考虑假设我们知道了这个数怎么求解答案。
显然可以套用刚才的模式,但是我们不好统计答案。所以考虑维护 \(sum[u][i]\) 表示以 \(u\) 为根的子树含有 \(2^i\) 这一位的数的个数。
那么统计答案直接使用这个值就好了。
需要注意:可能第 \(k\) 大值有重复的所以要去掉重复的部分。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 8e5+5;
const int MOD = 1e9+7;
int tot = 1,n,k,ch[N][2],sz[N],sum[N][32],a[100000];
int power(int a,int b){
int res = 1;
while(b){
if(b & 1) res = (res * a) % MOD;
a = (a * a) % MOD;
b >>= 1;
}
return res;
}
void insert(int x){
int now = 1;
for(int i=30; i>=0; i--){
int t = (x >> i) & 1;
if(!ch[now][t]) ch[now][t] = ++tot;
now = ch[now][t];sz[now]++;
}
}
int check(int x){ //异或值大于等于 x 的数对的个数
int ans = 0;
for(int i=1; i<=n; i++){
int u = 1; //a[i] = 0 -> 1
for(int j=30; j>=0; j--){
int t1 = (a[i] >> j) & 1;
int t2 = (x >> j) & 1;
if(!t2){
ans += sz[ch[u][t1^1]]; //(t1) ^ (t1 ^ 1) = 1
u = ch[u][t1]; //t2 = 0
}
else u = ch[u][t1^1]; //t2 = 1 t1 ^ t1 = 0
if(!u) break;
}
ans += sz[u];
}
return ans / 2;
//a1^a2 和 a2^a1
}
void dfs(int now,int dep,int z){ //sum[i][j] 以 i 为根的子树 2^j 这一位为 1 的数的个数
if(!now) return;
if(dep == 0){
for(int i=30; i>=0; i--){
if((z >> i)&1)
sum[now][i] = sz[now];
}
return;
}
dfs(ch[now][0],dep-1,z);
dfs(ch[now][1],dep-1,z | (1<<(dep-1)));
for(int i=0; i<=30; i++) sum[now][i] += sum[ch[now][0]][i] + sum[ch[now][1]][i];
}
int get_ans(int x){
int ans = 0;
for(int i=1; i<=n; i++){
int u = 1;
for(int j=30; j>=0; j--){
int t1 = (a[i] >> j) & 1;
int t2 = (x >> j) & 1;
if(!t2){
int tmp = ch[u][t1^1];
for(int k=0; k<=30; k++){
int p1 = (a[i] >> k) & 1;
if(p1) ans = (ans + (sz[tmp] - sum[tmp][k]) * (1ll<<k))%MOD;
else ans = (ans + sum[tmp][k] * (1ll<<k))%MOD;
}
u = ch[u][t1];
}
else u = ch[u][t1 ^ 1];
if(u == 0) break;
}
ans = (ans + sz[u] * x)%MOD; //等于 x 的部分
}
return ans;
}
int mod(int x){
return ((x % MOD)+MOD)%MOD;
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
// freopen("c.in","r",stdin);
// freopen("c.out","w",stdout);
scanf("%lld%lld",&n,&k);
if(k == 0){
printf("%lld\n",0ll);
return 0;
}
for(int i=1; i<=n; i++){
scanf("%lld",&a[i]);
insert(a[i]);
}
int l = 0,r = 1<<30,ans = 0;
while(l <= r){
int mid = (l + r)>>1;
if(check(mid) >= k){
ans = mid;l = mid + 1;
}
else r = mid - 1;
}
dfs(1,31,0);
int res = get_ans(ans); //a1 ^ a2 a2 ^ a1
res = (res * power(2,MOD-2))%MOD;
printf("%lld\n",mod(res - ((check(ans) - k) * ans)));
//前 k 大,check(ans) 大于等于第 k 大的个数,ans 就是第 k 大的值
return 0;
}
[国家集训队]拉拉队排练
题目分析:
显然可以使用 Manacher 求出每个点为中心的最长回文子串的长度。
但是我们求出来的是最长的,它其中可能包含着一些比较短的串,那直接考虑差分然后求一边前缀和就可以知道真实的长度为 \(i\) 的回文子串的数量了。
然后直接从后到前扫一遍快速幂求一下就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define L(i,a,b) for(int i=(a); i<=(b); i++)
#define R(i,a,b) for(int i=(b); i>=(a); i--)
#define me(a,x) memset(a,x,sizeof(a))
#define int long long
#define PII pair<int,int>
using namespace std;
const int N = 3e6+5;
const int MOD = 19930726;
char s[N];
int tot,r[N],sum[N];
int power(int a,int b){
int res = 1;
while(b){
if(b & 1) res = (res * a) % MOD;
a = (a * a) % MOD;
b >>= 1;
}
return res;
}
signed main(){
int n,k;
scanf("%lld%lld",&n,&k);
if(k == 0){
printf("0\n");
return 0;
}
string tmp;
cin>>tmp;
s[++tot] = '&';
for(int i=0; i<tmp.size(); i++) s[++tot] = '#',s[++tot] = tmp[i];
s[++tot] = '#';s[++tot] = '*';
int mx = 0,mid = 0;
for(int i=1; i<=tot; i++){
if(i <= mx) r[i] = min(r[2 * mid - i],mx - i);
while(s[i + r[i]] == s[i - r[i]] && i >= r[i]) r[i]++;
if(i + r[i] - 1 > mx){
mx = i + r[i] - 1;
mid = i;
}
}
int ans = 1;
for(int i=1; i<=tot; i++){
if(r[i] % 2 == 0){
sum[1]++;
sum[(r[i] - 1) + 1]--;
}
}
for(int i=1; i<=tot; i++){
sum[i] += sum[i-1];
}
for(int i=(tot & 1 ? tot : tot ^ 1); i>=1; i-=2){
if(k){
ans = (ans * power(i,min(k,sum[i]))) % MOD;
k -= min(k,sum[i]);
}
}
if(k) printf("%lld\n",-1ll);
else printf("%lld\n",ans);
return 0;
}
ABC270D Stones
题目分析:
这个题有一个显然地贪心用来迷惑人(这可能就是为啥比 E 通过人数还少吧),但是这个贪心是不对的。
所以就考虑 \(dp\),那么就是设 \(dp[i]\) 表示拿了前 \(i\) 个石子先手能取得的最大石子数。
转移:
代码:
点击查看代码
#include<bits/stdc++.h>
#define L(i,a,b) for(int i=(a); i<=(b); i++)
#define R(i,a,b) for(int i=(b); i>=(a); i--)
#define me(a,x) memset(a,x,sizeof(a))
#define int long long
#define PII pair<int,int>
using namespace std;
const int N = 10005;
int a[N],dp[N];
signed main(){
int n,k;
scanf("%lld%lld",&n,&k);
for(int i=1; i<=k; i++) scanf("%lld",&a[i]);
for(int i=0; i<=n; i++){
for(int j=1; j<=k; j++){
if(a[j] > i) break;
dp[i] = max(dp[i],i - dp[i - a[j]]);
}
}
printf("%lld\n",dp[n]);
return 0;
}
ABC270E Apple Baskets on Circle
题目分析:
我们可以显然发现题目具有二分单调性,也就是我们可以二分跑的圈数。
二分完成之后这个题目就可以非常简单地通过模拟过掉了
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define L(i,a,b) for(int i=(a); i<=(b); i++)
using namespace std;
const int N = 2e6+5;
int n,k,a[N];
bool check(int limit){
int ans = 0;
L(i,1,n)
ans += min(limit,a[i]);
return ans <= k;
}
signed main(){
scanf("%lld%lld",&n,&k);
L(i,1,n) scanf("%lld",&a[i]);
int l = 0,r = k + 1;
int ans = 0;
while(l <= r){
int mid = (l + r)>>1;
if(check(mid)){
ans = mid;
l = mid + 1;
}
else r = mid - 1;
}
L(i,1,n) k -= min(a[i],ans),a[i] -= min(a[i],ans);
L(i,1,n) if(a[i] && k) a[i] -- ,k --;
L(i,1,n) printf("%lld ",a[i]);
return 0;
}
ABC270F Transportation
题目分析:
我们可以发现这个题仿佛和最小生成树有点关系,都是选择最少的权值使得整个图联通。
我们考虑对于通过港口和机场联通的并不好处理,因为任意两个选择港口或机场的都可以直接联通。
但是我们也会发现如果两个点通过机场或港口联通必定是两个同时修建,所以就可以考虑分别新建一个节点从每个点向这个新建的点连边边权为修建机场或者港口的代价。
但是会发现这样还是过不了。
因为我们求最小生成树相当于必须选择一个机场和一个港口,那么为了避免这种情况我们就枚举是否要选择机场和港口,就可以了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define L(i,a,b) for(int i=(a); i<=(b); i++)
#define R(i,a,b) for(int i=(b); i>=(a); i--)
#define me(a,x) memset(a,x,sizeof(a))
#define int long long
using namespace std;
const int INF = 1e18+5;
const int N = 2e6+5;
struct edge{
int from,to,val;
}e[3 * N];
int n,m,tot,tmp = INF,fa[N],x[N],y[N],from[N],to[N],val[N];
int find(int x){
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
bool cmp(edge a,edge b){
return a.val < b.val;
}
void solve(int a,int b){
L(i,1,n + 3) fa[i] = i;
tot = 0;
L(i,1,m) e[++tot] = {from[i],to[i],val[i]};
if(a) L(i,1,n) e[++tot] = {i,n+1,x[i]};
if(b) L(i,1,n) e[++tot] = {i,n+2,y[i]};
sort(e+1,e+tot+1,cmp);
int ans = 0;
L(i,1,tot){
if(find(e[i].from) != find(e[i].to)){
fa[find(e[i].to)] = find(e[i].from);
ans += e[i].val;
}
}
L(i,1,n) if(find(i) != find(1)) return;
tmp = min(tmp,ans);
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%lld%lld",&n,&m);
L(i,1,n) scanf("%lld",&x[i]);
L(i,1,n) scanf("%lld",&y[i]);
L(i,1,m) scanf("%lld%lld%lld",&from[i],&to[i],&val[i]);
L(i,0,1) L(j,0,1) solve(i,j);
printf("%lld\n",tmp);
return 0;
}
ABC270G Sequence in mod P
题目分析:
这种题目很大概率会需要 BSGS 来解决。
我们会发现最短循环节长度肯定不会超过 \(p\),所以也就是对于 \(p\) 的范围做一次 BSGS。
也就是板子题了。
复杂度:\(O(\sqrt{p} \log p)\)
代码:
点击查看代码
#include<bits/stdc++.h>
#define L(i,a,b) for(int i=(a); i<=(b); i++)
#define R(i,a,b) for(int i=(b); i>=(a); i--)
#define me(a,x) memset(a,x,sizeof(a))
#define int long long
using namespace std;
const int N = 2e6+5;
int mod,a,b,s,g,x[N];
struct node{
int v,t;
node(int _v = 1,int _t = 0){
v = _v,t = _t;
}
};
node operator * (node a,node b){
return node((a.v * b.v)%mod, ((a.t * b.v)+b.t)%mod);
}
node power(node a,int b){
node res = node(1,0);
while(b){
if(b & 1) res = res * a;
a = a * a;
b >>= 1;
}
return res;
}
int power(int a,int b){
int res = 1;
while(b){
if(b & 1) res = (res * a) % mod;
a = (a * a) % mod;
b >>= 1;
}
return res;
}
int MOD(int x){
return ((x % mod)+mod)%mod;
}
int solve(){ //【模板】BSGS
scanf("%lld%lld%lld%lld%lld",&mod,&a,&b,&s,&g);
if(s == g) return 0;
if(a == 0){
if(b == g) return 1;
return -1;
}
map<int,int> mp;
int len = sqrt(mod);
int biga = 1,bigb = 0,now = g;
int inva = power(a,mod-2);
int invb = (((MOD(-b) * power(a,mod-2))%mod) + mod)%mod;
for(int i=0; i<len; i++){
if(!mp.count(now)) mp[now] = i;
now = (now * inva + invb)%mod;
biga = (biga * a)%mod;
bigb = (bigb * a)%mod;
bigb = (bigb + b)%mod;
}
now = s;
for(int i=0; i<mod/len+1; i++){
if(mp.count(now)) return i * len + mp[now];
now = (now * biga + bigb)%mod;
}
return -1;
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("ans.txt","w",stdout);
int t;
scanf("%lld",&t);
while(t--){
printf("%lld\n",solve());
}
return 0;
}