星期一:
昨晚熬夜场的div2总算是不负有心人,到C都比较简单,出C后我也没有run的想法,一直在看D,最后5min的时间ac,小小上了波分
贴cf round976 div2 D cf传送门
题意:给m次连接点的操作,问最后的连通块数
思路:暴力的复杂度是 n*m,但我们注意到一个很关键的信息 -- d<=10,马上就想到可以按d给操作分个类,对于不同的d,可以按不同的起点分类,例如d==3,有三个不同的起点,d==10,就有10种不同的起点
计算下共有385种不同的操作,若能对于每种操作,处理出其操作的区间,那么就能以 O(n*10)的复杂度处理完毕。在想到这后简单想了下如何处理区间之间的交,合并等操作,感觉并不太好实现(现在能实现了,先将区间排序),于是,上线段树!(区改,单查
对于每种操作,先找出操作的类型,然后对其操作的区间赋值,这就是线段树的目的。但会发现385种操作类型过于庞大,节点并不太好维护此信息(可以开个长度为10数组,每个值都是压缩的状态),我这里是分10次用线段树,对于起点为st的操作,给区间l,r赋上 1<<st的值
re了两发,wa了两发扣了有点多分
RE:没注意到 k==0时,我的l会大于r,给线段树操作就会寄
WA:太久没写并查集,合并写唐了
代码如下:
const int N=2e6+10,M=1e4+10;
const int INF=0x3f3f3f3f;
const int mod=1e9+7;
ll n;
struct seg_Tree{
#define lc p<<1
#define rc p<<1|1
struct nod{
int l,r; //385
ll opt,tag;
}t[N];
int opd,ql,qr,qv;
nod merge(nod a,nod b){
nod res;
res.l=a.l,res.r=b.r;
res.opt=a.opt|b.opt;
res.tag=0;
return res;
}
void pushup(int p){t[p]=merge(t[lc],t[rc]);}
void bd(int p,int l,int r){
t[p]={l,r,0,0};
if(l==r) return ;
int mid=l+r>>1;
bd(lc,l,mid);
bd(rc,mid+1,r);
}
void pushdn(int p){
if(!t[p].tag) return ;
t[lc].opt|=t[p].tag;
t[lc].tag|=t[p].tag;
t[rc].opt|=t[p].tag;
t[rc].tag|=t[p].tag;
t[p].tag=0;
}
void update(int p){
if(ql<=t[p].l && qr>=t[p].r){
t[p].opt|=(1<<qv);
t[p].tag|=(1<<qv);
return ;
}
int mid=t[p].l+t[p].r>>1;
pushdn(p);
if(ql<=mid) update(lc);
if(qr>mid) update(rc);
pushup(p);
}
void updt(int l,int r,int v){
ql=l,qr=r;
qv=v;
update(1);
}
nod query(int p){
if(ql<=t[p].l && qr>=t[p].r) return t[p];
int mid=t[p].l+t[p].r>>1;
pushdn(p);
if(qr<=mid) return query(lc);
if(ql>mid) return query(rc);
}
ll ask(int l,int r){
ql=l,qr=r;
return query(1).opt;
}
}tr;
vector<PII>op[11];
int fa[N];
int fnd(int x){
return fa[x]==x?x:fa[x]=fnd(fa[x]);
}
void solve(){
int m; cin >> n >> m;
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=10;i++) op[i].clear();
for(int i=1;i<=m;i++){
int a,d,k; cin >> a >> d >> k;
if(k) op[d].push_back({a,k});
}
for(int d=1;d<=10;d++){
tr.bd(1,1,n);
for(auto [a,k]:op[d]){
int l=a,r=a+(k-1)*d;
int st=(a-1)%d+1;
tr.updt(l,r,st); //若!k, r==l-d !!!会RE!!!
}
for(int st=1;st<=d;st++){
for(int i=st;i<=n;i+=d){
if(tr.ask(i,i)&1<<st){
int u=fnd(i),v=fnd(i+d);
// if(u!=v) fa[i]=fa[i+d]; //并查集合并不是这样写的!!!
if(u!=v) fa[u]=v;
}
}
}
}
ll ans=0;
for(int i=1;i<=n;i++) ans+=(fnd(i)==i);
cout << ans << "\n";
}
补cf round975 div2 D cf传送门
思路:猜个结论,答案一定是一段连续区间。然后想个贪心策略,每次前往还没征服的ai最小城市,由此可得到个解法,以一最小ai为起点,向左右二分check得到区间的左右端点,可惜没想到这个解法
官方的解法是,对于 i,找出最小的涵盖所有ai<=i的区间【l,r】,若此区间长度>i,则可直接得到答案为0,否则可得到对于这段区间,合法的起点范围为【r-i+1,l+i-1】,将所有起点区间求交集即可,这里用的差分,注意差分时 l不能小于1
代码如下:
const int N=2e6+10,M=1e4+10;
const int INF=0x3f3f3f3f;
const int mod=1e9+7;
ll n;
int a[N],pre[N],suf[N];
PII inv[N];
int sum[N];
void solve(){
cin >> n;
pre[0]=1e9;
for(int i=1;i<=n;i++){
cin >> a[i];
pre[i]=min(a[i],pre[i-1]);
sum[i]=0;
}
suf[n+1]=1e9;
for(int i=n;i;i--) suf[i]=min(a[i],suf[i+1]);
int l=0,r=0;
for(int i=1;i<=n;i++) if(a[i]==pre[n]){
if(!l) l=i;
r=i;
}
for(int i=pre[n];i<=n;i++){
while(pre[l-1]<=i) l--;
while(suf[r+1]<=i) r++;
inv[i]={l,r};
}
// for(int i=pre[n];i<=n;i++) cout << inv[i].first << " " << inv[i].second << "\n";
// cout << "\n";
for(int i=pre[n];i<=n;i++) if(inv[i].second-inv[i].first+1<=i){
int l=max(inv[i].second-i+1,1ll),r=inv[i].first+i-1;
sum[l]++,sum[r+1]--;
// cout << l << " " << r << "\n";
}
ll ans=0;
for(int i=1;i<=n;i++){
sum[i]+=sum[i-1];
ans+=(sum[i]==n-pre[n]+1);
// cout << sum[i] << " \n"[i==n];
}
cout << ans << "\n";
}
补补队友a的签到构造 cf传送门
思路:首先每行每列起码得放俩,放什么位置比较方便呢,易想到 (1,1), (1,2), (2,2), (2,3), (3,3)...的方法,最后一个放(n,1),观察一下发现这些数刚好是俩俩挨着的,再想到 num和num+1互质,即可得到按上述位置放从1到2*n的构造方法,此时所有行列gcd都为1
多的数随便放就行,注意判别放重了,特别注意(n,1)这个点,在这wa了一发
代码如下:
const int N=2e6+10,M=1e4+10;
const int INF=0x3f3f3f3f;
const int mod=1e9+7;
ll n;
void solve(){
ll k; cin >> n >> k;
int x=1,y=1;
while(x<=n && y<=n){
cout << x << " " << y << "\n";
if(x==y) y++;
else x++;
}
cout << n << " " << 1 << "\n";
ll num=n*2+1;
for(int x=1;x<=n && num<=k;x++){
for(int y=1;y<=n && num<=k;y++){
if(y==x || y==x+1 || (x==n && y==1)) continue;
cout << x << " " << y << "\n";
num++;
}
}
}
星期二:
国庆长假,好好把握😽😽💪
24牛客国庆集训1 J 牛客传送门
题意:给一完全图,边分黑白两色,问有多少三角形满足三条边颜色相同
思路:暴力枚举复杂度为 n^3。观察下也许能发现一个不合法的三角形的特点,即边一定为两黑一白或两白一黑,再看看点,发现三个点中只有一个点两条边颜色相同,其余两点连接的边均为一白一黑。那么对于点 a,包含它的不合法三角形的个数即为 (a的黑边数)*(a的白边数),所以可以用一变量ms遍历一遍点集计算所有不合法三角形的个数再用C n 3减去,但注意一个不合法三角形有两个这样的点,也就是会被减去两次,所以ms需要除2
代码如下:
bool edge[8005][8005];
int cnt0[N],cnt1[N];
void solve(){
int seed; cin >> n >> seed;
srand(seed);
for (int i = 1; i <= n; i++)
for (int j = i + 1; j <= n; j++){
edge[j][i] = edge[i][j] = read();
if(edge[i][j]) cnt0[i]++,cnt0[j]++;
else cnt1[i]++,cnt1[j]++;
}
ll ans=n*(n-1)*(n-2)/6,ms=0;
for(int i=1;i<=n;i++) ms+=1ll*cnt0[i]*cnt1[i];
cout << ans-ms/2;
}
补 24百度之星决赛 豆腐店 mtj传送门
那道树上差分,毁了我的国三梦
思路:很典的树上差分(但当时不会,这里是对边差分,和对点有一丢区别,修路用优先队列跑就行,因为每次都是除2,复杂度最多 n*logw
代码如下:
const int N=3e5+10,M=1e4+10;
const int INF=0x3f3f3f3f;
const int mod=1e9+7;
ll n;
vector<PII>ve[N];
int fa[N][20],dep[N],cf[N];
void dfs(int x,int f){
dep[x]=dep[f]+1;
fa[x][0]=f;
for(int j=1;j<20;j++) fa[x][j]=fa[fa[x][j-1]][j-1];
for(auto [w,v]:ve[x]) if(v!=f) dfs(v,x);
}
int lca(int u,int v){
if(dep[u]<dep[v]) swap(u,v);
for(int i=19;~i;i--)
if(dep[fa[u][i]]>=dep[v]) u=fa[u][i];
if(u==v) return v;
for(int i=19;~i;i--)
if(fa[u][i]!=fa[v][i]) u=fa[u][i],v=fa[v][i];
return fa[u][0];
}
struct nod{
ll w,t;
bool operator <(const nod &b)const{
return (w-w/2)*t<(b.w-b.w/2)*b.t;
}
};
priority_queue<nod>pq;
void dfs2(int x,int f){
for(auto [w,v]:ve[x]) if(v!=f){
dfs2(v,x);
pq.push({w,cf[v]});
cf[x]+=cf[v];
}
}
void solve(){
ll m,k; cin >> n >> m >> k;
for(int i=1;i<n;i++){
ll u,v,w; cin >> u >> v >> w;
ve[u].push_back({w,v});
ve[v].push_back({w,u});
}
dfs(1,0);
int lst=1;
while(m--){
int a; cin >> a;
int an=lca(lst,a);
// cout << lst << " " << a << " " << an << "\n";
cf[lst]++,cf[a]++,cf[an]-=2;
lst=a;
}
dfs2(1,0);
while(k-- && !pq.empty()){
auto [w,t]=pq.top(); pq.pop();
w/=2;
if(w) pq.push({w,t});
}
ll ans=0;
while(!pq.empty()){
auto [w,t]=pq.top(); pq.pop();
ans+=w*t;
}
cout << ans << "\n";
}
cf edu round2 E 线段树合并 cf传送门
线段树合并的复杂度:对于插入m个点,时间复杂度与空间复杂度都为 mlogm
思路:可称得上是线段树合并板子题,对每个点建一颗线段树,dfs时把所有子节点合并到父节点上,再插入父节点颜色,即可查询答案
因为ans没开ll又wa了两发,真是无语了
代码如下:
const int N=2e5+10,M=1e4+10;
const int INF=0x3f3f3f3f;
const int mod=1e9+7;
ll n;
struct seg_Tree{
#define lc(p) t[p].ch[0]
#define rc(p) t[p].ch[1]
struct nod{
int ch[2];
ll sum,an;
}t[N*22];
int tot,root[N];
void pushup(int p){
if(t[lc(p)].sum>t[rc(p)].sum)
t[p].sum=t[lc(p)].sum,t[p].an=t[lc(p)].an;
else if(t[lc(p)].sum<t[rc(p)].sum)
t[p].sum=t[rc(p)].sum,t[p].an=t[rc(p)].an;
else t[p].sum=t[lc(p)].sum,t[p].an=t[lc(p)].an+t[rc(p)].an;
}
int merge(int x,int y,int l,int r){
if(!x || !y) return x+y;
if(l==r){
t[y].sum+=t[x].sum;
t[y].an=l;
return y;
}
int mid=l+r>>1;
lc(y)=merge(lc(x),lc(y),l,mid);
rc(y)=merge(rc(x),rc(y),mid+1,r);
pushup(y);
return y;
}
void update(int &y,int l,int r,int v){
if(!y) y=++tot;
// t[y].sum++; //不是主席树写法
if(l==r){
t[y].sum++;
t[y].an=l;
return ;
}
int mid=l+r>>1;
if(v<=mid) update(lc(y),l,mid,v);
if(v>mid) update(rc(y),mid+1,r,v);
pushup(y);
}
}tr;
vector<int>ve[N];
ll c[N],ans[N]; 怎么又没开ll啊啊啊啊啊啊
void dfs(int x,int f){
for(int v:ve[x]) if(v!=f){
dfs(v,x);
tr.root[x]=tr.merge(tr.root[v],tr.root[x],1,n); //子节点直接和父节点合并
}
tr.update(tr.root[x],1,n,c[x]); //插入父节点新值
ans[x]=tr.t[tr.root[x]].an;
}
void solve(){
cin >> n;
for(int i=1;i<=n;i++) cin >> c[i];
for(int i=1;i<n;i++){
int u,v; cin >> u >> v;
ve[u].push_back(v);
ve[v].push_back(u);
}
dfs(1,0);
for(int i=1;i<=n;i++) cout << ans[i] << " \n"[i==n];
}
星期三:
国庆长假第二天😸😸😸
学而时习之,不亦乐乎 cf传送门
这题最少需开3e5*152个节点,但复杂度应是q*2logk,就是3e5*121才对,为什么至少得开3e5*152呢???
复杂度算错了,区间修改每次最多开两条链也就是2*logv,再算上pushdn,每次最多开近4条链,也就是q*4*logv的复杂度,这样算来最多需3e5*240个节点,那120过不了也合理了
洛谷 P4556 线段树合并板子题 洛谷传送门
思路:树上点差分+线段树合并的板子
注意差分过程的时间/空间复杂度为 m*logMAXN*4,因为一次差分要动4个点
dfs2处理差分的函数,内部的dfs2给写成dfs了,真的是耽误了好久。。可能是没睡够脑子不太清醒
代码如下:
const int N=3e5+10,M=210;
const int INF=0x3f3f3f3f;
const int mod=998244353;
ll n;
const int MAXN=1e5+10;
struct seg_Tree{
#define lc(p) t[p].ch[0]
#define rc(p) t[p].ch[1]
struct nod{
int ch[2];
ll an,sum;
}t[N*22];
int tot,root[N];
void pushup(int p){
if(t[lc(p)].sum>=t[rc(p)].sum) t[p].sum=t[lc(p)].sum,t[p].an=t[lc(p)].an;
else t[p].sum=t[rc(p)].sum,t[p].an=t[rc(p)].an;
}
int merge(int x,int y,int l,int r){
if(!x || !y) return x+y;
if(l==r){
t[y].sum+=t[x].sum;
return y;
}
int mid=l+r>>1;
lc(y)=merge(lc(x),lc(y),l,mid);
rc(y)=merge(rc(x),rc(y),mid+1,r);
pushup(y);
return y;
}
void update(int &y,int l,int r,int z,int v){
if(!y) y=++tot;
if(l==r){
t[y].sum+=v;
t[y].an=z;
return ;
}
int mid=l+r>>1;
if(z<=mid) update(lc(y),l,mid,z,v);
if(z>mid) update(rc(y),mid+1,r,z,v);
pushup(y);
}
}tr;
vector<int>ve[N];
int fa[N][20],dep[N];
void dfs(int x,int f){
dep[x]=dep[f]+1;
fa[x][0]=f;
for(int j=1;j<20;j++) fa[x][j]=fa[fa[x][j-1]][j-1];
for(int v:ve[x]) if(v!=f) dfs(v,x);
}
int lca(int u,int v){
if(dep[u]<dep[v]) swap(u,v);
for(int j=19;~j;j--)
if(dep[fa[u][j]]>=dep[v]) u=fa[u][j];
if(u==v) return v;
for(int j=19;~j;j--)
if(fa[u][j]!=fa[v][j]) u=fa[u][j],v=fa[v][j];
return fa[u][0];
}
ll ans[N];
void dfs2(int x,int f){
for(int v:ve[x]) if(v!=f){
dfs2(v,x); //一开始写成dfs(v,x),de了半天啊啊啊啊啊
tr.root[x]=tr.merge(tr.root[v],tr.root[x],1,MAXN);
}
ans[x]=tr.t[tr.root[x]].sum>0?tr.t[tr.root[x]].an:0;
}
void solve(){
int m; cin >> n >> m;
for(int i=1;i<n;i++){
int u,v; cin >> u >> v;
ve[u].push_back(v);
ve[v].push_back(u);
}
dfs(1,0);
while(m--){
int x,y,z; cin >> x >> y >> z;
int an=lca(x,y);
tr.update(tr.root[x],1,MAXN,z,1);
tr.update(tr.root[y],1,MAXN,z,1);
tr.update(tr.root[an],1,MAXN,z,-1);
tr.update(tr.root[fa[an][0]],1,MAXN,z,-1);
}
dfs2(1,0);
for(int i=1;i<=n;i++) cout << ans[i] << "\n";
}
23年秦皇岛的 I怎么这么难补,学完线段树合并又要去看分裂,我要成为数据结构领域大神!!!
洛谷 P5494 洛谷传送门
思路:线段树分裂板子题
注意函数是将前k个之后的数分裂出去,不是将前k个数分裂出去
代码如下:
const int N=3e5+10,M=210;
const int INF=0x3f3f3f3f;
const int mod=998244353;
ll n;
struct seg_Tree{
#define lc(p) t[p].ch[0]
#define rc(p) t[p].ch[1]
struct nod{
int ch[2];
ll sum;
}t[N*22];
int tot,root[N];
void pushup(int p){
t[p].sum=t[lc(p)].sum+t[rc(p)].sum;
}
int merge(int x,int y,int l,int r){
if(!x || !y) return x+y;
if(l==r){
t[y].sum+=t[x].sum;
return y;
}
int mid=l+r>>1;
lc(y)=merge(lc(x),lc(y),l,mid);
rc(y)=merge(rc(x),rc(y),mid+1,r);
pushup(y);
return y;
}
void split(int x,int &y,ll k){ //将x的前k个数之后的数分裂给y
if(!x) return ;
y=++tot;
ll s=t[lc(x)].sum;
if(k<=s) split(lc(x),lc(y),k),swap(rc(x),rc(y));
else split(rc(x),rc(y),k-s);
t[y].sum=t[x].sum-k,t[x].sum=k;
}
void update(int &p,int l,int r,int v,int k){
if(!p) p=++tot;
if(l==r){
t[p].sum+=k;
return ;
}
int mid=l+r>>1;
if(v<=mid) update(lc(p),l,mid,v,k);
else update(rc(p),mid+1,r,v,k);
pushup(p);
}
ll ask(int p,int l,int r,int ql,int qr){
if(!p) return 0;
if(ql<=l && qr>=r) return t[p].sum;
int mid=l+r>>1;
ll res=0;
if(ql<=mid) res+=ask(lc(p),l,mid,ql,qr);
if(qr>mid) res+=ask(rc(p),mid+1,r,ql,qr);
return res;
}
int fnd(int p,int l,int r,int k){
if(l==r) return l;
ll s=t[lc(p)].sum;
if(s+t[rc(p)].sum<k) return -1;
int mid=l+r>>1;
if(k<=s) return fnd(lc(p),l,mid,k);
else return fnd(rc(p),mid+1,r,k-s);
}
}tr;
void solve(){
int m; cin >> n >> m;
int cnt=1;
for(int i=1;i<=n;i++){
int k; cin >> k;
tr.update(tr.root[1],1,n,i,k);
}
while(m--){
int op,p; cin >> op >> p;
if(!op){
int x,y; cin >> x >> y;
ll sum1=tr.ask(tr.root[p],1,n,1,x-1);
ll sum2=tr.ask(tr.root[p],1,n,x,y);
tr.split(tr.root[p],tr.root[++cnt],sum1);
int tmp=0;
tr.split(tr.root[cnt],tmp,sum2); //cnt只保留前sum2个,剩余的还回去
tr.root[p]=tr.merge(tmp,tr.root[p],1,n);
}else if(op==1){
int t; cin >> t;
tr.root[p]=tr.merge(tr.root[t],tr.root[p],1,n);
}else if(op==2){
int x,q; cin >> x >> q;
tr.update(tr.root[p],1,n,q,x);
}else if(op==3){
int x,y; cin >> x >> y;
cout << tr.ask(tr.root[p],1,n,x,y) << "\n";
}else{
int k; cin >> k;
cout << tr.fnd(tr.root[p],1,n,k) << "\n";
}
}
}
cf round312 div2 E cf传送门
思路:嗯开26棵线段树,给每个字母都开一棵,对于叶子节点意义是 s【l】是否是此字母
需要支持区修区查,这里用的动态开点
代码如下:
const int N=1e5+10,M=210;
const int INF=0x3f3f3f3f;
const int mod=998244353;
ll n;
struct seg_Tree{
#define lc(p) t[p].ch[0]
#define rc(p) t[p].ch[1]
struct nod{
int ch[2];
int sum,tag;
}t[N*4*26];
int tot,root[27];
void pushup(int p){
t[p].sum=t[lc(p)].sum+t[rc(p)].sum;
t[p].tag=0;
}
void pushdn(int p,int l,int r){
if(!t[p].tag) return ;
if(!lc(p)) lc(p)=++tot;
if(!rc(p)) rc(p)=++tot;
int mid=l+r>>1;
if(t[p].tag==1){
t[lc(p)].sum=mid-l+1;
t[lc(p)].tag=1;
t[rc(p)].sum=r-mid;
t[rc(p)].tag=1;
}else{
t[lc(p)].sum=0;
t[lc(p)].tag=-1;
t[rc(p)].sum=0;
t[rc(p)].tag=-1;
}
t[p].tag=0;
}
void update(int &p,int l,int r,int ql,int qr,int qop){
if(!p) p=++tot;
if(ql<=l && qr>=r){
if(qop==1) t[p].sum=r-l+1,t[p].tag=1;
else t[p].sum=0,t[p].tag=-1;
return ;
}
int mid=l+r>>1;
pushdn(p,l,r);
if(ql<=mid) update(lc(p),l,mid,ql,qr,qop);
if(qr>mid) update(rc(p),mid+1,r,ql,qr,qop);
pushup(p);
}
int ask(int p,int l,int r,int ql,int qr){
if(!p) return 0;
if(ql<=l && qr>=r) return t[p].sum;
int mid=l+r>>1;
int res=0;
pushdn(p,l,r);
if(ql<=mid) res+=ask(lc(p),l,mid,ql,qr);
if(qr>mid) res+=ask(rc(p),mid+1,r,ql,qr);
return res;
}
}tr;
void solve(){
int q; cin >> n >> q;
string s; cin >> s;s=" "+s;
for(int i=1;i<=n;i++){
int j=s[i]-'a'+1;
tr.update(tr.root[j],1,n,i,i,1);
}
while(q--){
int l,r,op; cin >> l >> r >> op;
int c[27]={0}; //统计区间内26个字母的数量
for(int i=1;i<=26;i++){
c[i]=tr.ask(tr.root[i],1,n,l,r);
tr.update(tr.root[i],1,n,l,r,-1);
}
int from=l;
for(int i=op?1:26;op?i<=26:i;op?i++:i--) if(c[i]){
int to=from+c[i]-1;
tr.update(tr.root[i],1,n,from,to,1); //升序或降序放
from=to+1;
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=26;j++)
if(tr.ask(tr.root[j],1,n,i,i)==1){cout << char('a'+j-1); break;}
}
}
星期四:
国庆长假第三天🤨 转眼已过半
23 CCPC秦皇岛 I,目前补不动😭,以后再看
23 ICPC济南 队友出的A cf传送门
思路:刚开始以为只能有一个嵌套,如oxo xx oxo形式(ox代表方圆类型,有点莽撞地交了发wa2,确实是有点莽了,只要稍稍再想想,很容易想到反例,如ooxx,oxxoxx。
实际上是最多俩嵌套,而且俩嵌套最外括号得不同,若最外括号相同,如([()]) (),可变为([()] () ),一眼假。若存在仨嵌套,那么由前面可知,第一坨和第三坨最外括号一定相同,还是一眼假
代码如下:
const int N=1e5+10,M=210;
const int INF=0x3f3f3f3f;
const int mod=998244353;
ll n;
void solve(){
string s; cin >> s;
n=s.size(); s=" "+s;
for(int i=1;i<=n;i++){
if(s[i]=='(' || s[i]==')') s[i]='o';
else s[i]='x';
}
int cnt=0;
for(int i=1;i<n;i++) cnt+=(s[i]==s[i+1]);
if(cnt>2) cout << "No\n";
else cout << "Yes\n";
}
星期五:
和队友打了24牛客国庆集训4,被扫描线拿下了,赶紧学了补
补 23 ICPC济南 G cf传送门
据说这种两两冲突求方案数是典题,好好体会下。
思路:虽说我用的并查集,但这种题还是转成图论比较好,把每行分为 i表示翻转和 i+n不翻转,将有冲突的选择连边,一条边的两点选其一,跑个二分图染色。
先将 i和 i+n连边,然后遍历 i和 c-i+1列,若sum1>2,则无解(注意若奇数列特判中间那列), 遍历列和其镜像列,若1的个数<2,则每行翻不翻无所谓。若==2,若俩1在同一列,则两行翻转状态必须相反,否则必须相同。连完边跑个二分图染色,若没有任何翻转的限制答案为 2^r,建图后每个连通块的选项绑定在一起,答案为 2^连通块个数
代码如下:
const int N=2e6+10,M=210;
const int INF=0x3f3f3f3f;
const int mod=1e9+7;
ll n;
string b[N];
int fa[N];
int fnd(int x){
return fa[x]==x?x:fa[x]=fnd(fa[x]);
}
void merge(int u,int v){
u=fnd(u),v=fnd(v);
if(u!=v) fa[u]=v;
}
ll qpow(ll a,ll n){
ll res=1;
while(n){
if(n&1) (res*=a)%=mod;
(a*=a)%=mod;
n>>=1;
}
return res;
}
void solve(){
ll c; cin >> n >> c;
for(int i=1;i<=2*n;i++) fa[i]=i;
for(int i=1;i<=n;i++) cin >> b[i],b[i]=" "+b[i];
if(c&1){
int mid=0;
for(int i=1;i<=n;i++) mid+=(b[i][c/2+1]=='1');
if(mid>1){cout << "0\n"; return ;}
}
for(int l=1,r=c;l<r;l++,r--){
int sum=0;
for(int i=1;i<=n;i++) sum+=(b[i][l]=='1')+(b[i][r]=='1');
if(sum>2){cout << "0\n"; return ;}
}
for(int l=1,r=c;l<r;l++,r--){
vector<int>vel,ver;
for(int i=1;i<=n;i++){
if(b[i][l]=='1') vel.push_back(i);
if(b[i][r]=='1') ver.push_back(i);
}
if(vel.size()==2) merge(vel[0],vel[1]+n),merge(vel[0]+n,vel[1]);
else if(ver.size()==2) merge(ver[0],ver[1]+n),merge(ver[0]+n,ver[1]);
else if(vel.size()==1 && ver.size()==1)
merge(vel[0],ver[0]),merge(vel[0]+n,ver[0]+n);
}
for(int i=1;i<=n;i++) if(fnd(i)==fnd(i+n)){cout << "0\n"; return ;}
int cnt=0;
for(int i=1;i<=n;i++) cnt+=(fnd(i)==i);
cout << qpow(2,cnt) << "\n";
}
星期六:
补 cf round806 div4 G cf传送门
思路:比较典的线性dp,关键在于除2这个操作,见到这个字眼应该格外警惕,因为除2很快,任何值的log2都很小,所以1e9的值拿30次bad key后就会变成0,由此可以对每个 i枚举拿bk的次数
dp【i】【j】表示考虑到第 i个箱子用了 j次bk的最大值,注意这题需要时刻取max
赛时很快猜到了个结论,即如用bk那么存在一个idx,在<=idx时都用gk,在idx后至n都用bk,可惜当时以为是三分,后来发现用bk的时机并非二次函数。实际上有了这个结论后可以直接暴力枚举时机,而且最多往后枚举30个,可惜当时还是没有想到log21e9在30以内,误以为是 n^2写法 : <
代码如下:
const int N=2e5+10,M=4e3+10;
const int INF=0x3f3f3f3f;
const int mod=1e9+7;
inline ll rng(ll l,ll r){return rnd()%(r-l+1)+l;}
ll n;
ll a[N],k;
ll dp[N][33];
void solve(){
cin >> n >> k;
for(int i=1;i<=n;i++) cin >> a[i];
ll ans=0;
for(int i=1;i<=n;i++){
ll ta=a[i];
for(int j=0;j<=min(i,31);j++){
if(!j) dp[i][j]=dp[i-1][j]+ta-k;
else dp[i][j]=max(dp[i-1][j]+ta-k,dp[i-1][j-1]+ta);
ta>>=1;
ans=max(dp[i][j],ans);
}
}
cout << ans << "\n";
}
补 cf round926 E cf传送门
思路:一眼树上差分+状压,但是对于状压有些许问题。
最开始是边处理差分边跑状压,只跑出现过的有限的状态,但这样有时会达不到答案,也想过先把边提出来再跑一个状压,但由于不清楚不同状态的边的数量,感觉会T,没敢跑(现在想来,赛时那么多wa2,就应该换个写法莽一发),赛后发现实际上不同状态的边的数量是 k级别的,这样就可以很轻松跑个状压,但是对于为何这么少还没想明白,说是虚树,现在不是很想看
代码如下:
const int N=1e5+10,M=1.1e6+10;
const int INF=0x3f3f3f3f;
const int mod=1e9+7;
inline ll rng(ll l,ll r){return rnd()%(r-l+1)+l;}
ll n;
vector<int>ve[N];
int dep[N],fa[N][20];
int cf[N];
set<int>ma;
int dp[M];
void dfs(int x,int f){
dep[x]=dep[f]+1;
fa[x][0]=f;
for(int j=1;j<20;j++) fa[x][j]=fa[fa[x][j-1]][j-1];
for(int v:ve[x]) if(v!=f) dfs(v,x);
}
int lca(int u,int v){
if(dep[u]<dep[v]) swap(u,v);
for(int j=19;~j;j--)
if(dep[fa[u][j]]>=dep[v]) u=fa[u][j];
if(u==v) return v;
for(int j=19;~j;j--)
if(fa[u][j]!=fa[v][j]) u=fa[u][j],v=fa[v][j];
return fa[u][0];
}
void dfs2(int x,int f){
for(int v:ve[x]) if(v!=f){
dfs2(v,x);
cf[x]+=cf[v];
}
ma.insert(cf[x]);
}
void solve(){
cin >> n;
ma.clear();
for(int i=1;i<=n;i++){
ve[i].clear();
cf[i]=0;
}
int rt=0;
for(int i=1;i<n;i++){
int u,v; cin >> u >> v;
ve[u].push_back(v);
ve[v].push_back(u);
}
for(int i=1;i<=n;i++) if(ve[i].size()==1){rt=i; break;}
dfs(rt,0);
int k; cin >> k;
for(int i=1;i<1<<k;i++) dp[i]=1e9;
for(int i=0;i<k;i++){
int a,b; cin >> a >> b;
int an=lca(a,b);
cf[a]+=1<<i,cf[b]+=1<<i;
cf[an]-=(1<<i+1);
}
dfs2(rt,0);
for(int mask=0;mask<1<<k;mask++){
for(int m:ma){
int nmask=mask|m;
dp[nmask]=min(dp[mask]+1,dp[nmask]);
}
}
cout << dp[(1<<k)-1] << "\n";
}
周日:
下午vp 21 CCPC威海,中途想双线程开div2,被A吓跑了
补cf edu round164 div2 D cf传送门
参考题解:D. Colored Balls - onlyblues - 博客园 (cnblogs.com)
思路:先考虑如何计算每个颜色集的对答案贡献,假设颜色集总球数为sum,若球数最多的颜色(记max)不严格大于sum/2,则答案为 sum/2向上取整,否则为max。
将 a从小到大排序,dp【i】【j】表示考虑到第 i个拿了 j个球的方案数(不是必拿 i
dp的转移很好写,考虑答案怎么计算
分两种情况:( i从1到n遍历,因为已经排了序,此时 ai即最大球数,此时讨论的是dp【i-1】【j】
1.若 ai > j,即 ai > (j+ai)/2,则贡献为 ai,方案数为 dp【i-1】【j】
2.若 ai <= j,则贡献为 (ai+j)/2向上取整,方案数同上
代码如下:
const int N=5e5+10,M=1.1e6+10;
const int INF=0x3f3f3f3f;
const int mod=998244353;
inline ll rng(ll l,ll r){return rnd()%(r-l+1)+l;}
ll n;
ll a[5050];
ll dp[5050][5050];
void solve(){
cin >> n;
for(int i=1;i<=n;i++) cin >> a[i];
sort(a+1,a+n+1);
ll ans=0;
dp[0][0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<=5000;j++){
if(j>=a[i]) (ans+=(j+a[i]+1)/2*dp[i-1][j])%=mod;
else (ans+=dp[i-1][j]*a[i])%=mod;
dp[i][j]=dp[i-1][j];
if(j>=a[i]) (dp[i][j]+=dp[i-1][j-a[i]])%=mod;
}
}
cout << ans;
}
星期一:
洛谷P5490 扫描线 洛谷传送门
思路:纯板子,看的董晓算法
有很多细节需要注意,主要是计算节点长度时,节点右端点应向右偏移一位,因为若不这么做,那么叶子节点的l==r,难道叶子节点代表的长度即x【r】-x【l】即为0吗?显然不能这样,所以需要偏移。离散量共有s个,因为偏移了,建树时也只需建1 - s-1即可。且更新时也要注意,因为计算时向右偏了一位,所以传参时 r要-1
代码如下:
const int N=2e5+10,M=1.1e6+10;
const int INF=0x3f3f3f3f;
const int mod=998244353;
inline ll rng(ll l,ll r){return rnd()%(r-l+1)+l;}
ll n;
struct seg_Tree{
#define lc p<<1
#define rc p<<1|1
struct line{ //扫描线
ll x1,x2,y;
int tag;
bool operator <(const line &b)const{return y<b.y;}
}l[N];
struct nod{
int l,r;
ll cnt,len;
}t[N<<3];
ll x[N],ql,qr,qv;
void pushup(int p){
int l=t[p].l,r=t[p].r;
if(t[p].cnt) t[p].len=x[r+1]-x[l];
else t[p].len=t[lc].len+t[rc].len;
}
void bd(int p,int l,int r){
t[p]={l,r,0,0};
if(l==r) return ;
int mid=l+r>>1;
bd(lc,l,mid);
bd(rc,mid+1,r);
}
void update(int p){
if(ql<=t[p].l && qr>=t[p].r){
t[p].cnt+=qv;
pushup(p);
return ;
}
int mid=t[p].l+t[p].r>>1;
if(ql<=mid) update(lc);
if(qr>mid) update(rc);
pushup(p);
}
void upd(int l,int r,int v){
ql=l,qr=r;
qv=v;
update(1);
}
}tr;
void solve(){
cin >> n;
for(int i=1;i<=n;i++){
int x1,y1,x2,y2; cin >> x1 >> y1 >> x2 >> y2;
tr.l[i]={x1,x2,y1,1};
tr.l[i+n]={x1,x2,y2,-1};
tr.x[i]=x1,tr.x[i+n]=x2;
}
n<<=1;
sort(tr.l+1,tr.l+n+1);
sort(tr.x+1,tr.x+n+1);
ll s=unique(tr.x+1,tr.x+n+1)-tr.x-1;
tr.bd(1,1,s-1); //注意-1
ll ans=0;
for(int i=1;i<n;i++){
int l=lower_bound(tr.x+1,tr.x+s+1,tr.l[i].x1)-tr.x;
int r=lower_bound(tr.x+1,tr.x+s+1,tr.l[i].x2)-tr.x;
tr.upd(l,r-1,tr.l[i].tag); //注意-1
ans+=tr.t[1].len*(tr.l[i+1].y-tr.l[i].y);
}
cout << ans;
}
在这放个很顶的哈希方法,参考自:一种比较科学的字符串哈希实现方法 - 知乎 (zhihu.com)
洛谷U461211 洛谷传送门
代码如下:
const int N=2e6+10,M=3e3+10;
const int INF=0x3f3f3f3f;
const int mod=1e9+7;
inline ll rng(ll l,ll r){return rnd()%(r-l+1)+l;}
ll n;
const int base=131;
const ull MOD=(1ull<<61)-1;
inline ull add(ull a,ull b){
a+=b;
if(a>=MOD) a-=MOD;
return a;
}
inline ull mul(ull a,ull b){
__uint128_t c=__uint128_t(a)*b;
return add(c>>61,c&MOD);
}
void solve(){
cin >> n;
set<ull>st;
while(n--){
string s; cin >> s;
ull hsh=0;
for(auto i:s) hsh=(add(mul(hsh,base),i));
st.insert(hsh);
}
cout << st.size();
}