暑期集训1
T1:玩游戏:思维+贪心
T2:排列:笛卡尔树DP(其实就是把问题通过一个方式分为左子区间和右子区间分别处理)
T3:最短路:神奇双向同时Dij最短路
T4:矩形:线段树标记和区间处理在二维问题上灵活应用
T1:给你一个数列A,和位置pos,要求从pos位置开始,双指针l,r从pos开始,每次l可以--,r可以++,要求任意时刻
满足sigma(a[i])[l<i<=r]<=0,问你是否存在一种移动顺序使得l移动到1,r移动到n
贪心策略:
从k位置开始,每次向左向右选到第一个小于当前sum的位置,如果有,就跳过去;如果没有,就不跳
在尝试完左右之后,如果都没跳,说明根本走不动,无论是左右,只要一走,就会>0,就return 0;如果没跳是因为已经到终点了,就reutrn 1
如果到终点但是最后合并不了,return 0
注意必须是选到第一个小于的就溜(其实不是第一个也行,但是我判断继续的条件就是<=0就行,所以不能及时止损),因为你不知道下一个小的到底在什么时候,不然
1
7 3
9 3 -3 -2 4 1 -20
这个数据,你左边3一旦在4之前+上,就不可能合法了
为什么这种贪心是对的:
本来我是想如果对于单点是负数我就+,是正数就不知道怎么办了,但是,无论是正数还是负数,我只关心对于当前答案的影响,如果是正数,我就往后找到第一个有负数可以抵消正数贡献的,
同时还保证过程中sum<=0合法,这就一定没问题
O(n)
const int N=100;
int t,n,k;
ll a[100000+100];
inline bool deal()
{
int l=k+1,r=k;ll sum=0;
while(sum<=0)
{
bool ok=0;
ll lres=sum,rres=sum;ll lt=l,rt=r;
while(lt>2&&lres>=sum)
{
if(lres+a[lt-1]<=0)lres+=a[lt-1],--lt;
else break;
}
if(lres<sum)ok=1,sum=rres=lres,l=lt;
while(rt<n&&rres>=sum)
{
if(rres+a[rt+1]<=0)rres+=a[rt+1],++rt;
else break;
}
if(rres<sum)ok=1,sum=rres,r=rt;
if(!ok)//没更新
{
if(lt>2||rt<n||(rres+lres-sum)>0)return false;
//对应都没更新,(1)向左走不了(2)向右走不了(3)局部走到,但是合并不了
return true;
}
if(l==2&&r==n)return true;
}
return 0;
}
int main()
{
t=re();
_f(qwertyuiop,1,t)
{
n=re(),k=re();
_f(i,1,n)a[i]=re();
if(deal())chu("Yes\n");
else chu("No\n");
}
return 0;
}
T2:给你两个正整数n,k,要求你生成n的排列,对于这个排列,每次的操作是删除序列中的“相对小值”,“相对小值”的定义是对于这个数,存在和他相邻的数
>他。问你有多少种n的排列,输出方案数(n<=1000,k<=1000)
分治的思想,假如我确定了n序列的max,max把n序列分成相互独立的两个部分,两个部分不会相互影响,因为max一定是最后剩下或者最后删去的数。
对于左右的区间,还可以枚举max,分区间,直到最简单的问题。设dp[i][j][0/1][0/1]是长度是i的序列,至多j次操作后只剩最值,左区间左边界的上一位存在最大值(0),右边界下一位存在最大值(1)
,右区间同,的方案数。我们考虑合并一个两个区间变成一个。(至多是一个前缀和优化,省略掉还要枚举的q,p俩维度)。
如果区间左右边界之外没有max(原始区间),那么枚举区间内max_p位置,左子区间和右子区间的max共用max_p,不需要再次合并,因为max左右的数可以随便选
,乘上组合数C[i-1][k-1],也正是这里枚举完了放的数,所以数之间的大小已经确定了,max就是确定的,不能再枚举;
如果左边有边界,那中间的max_p必须删,而且只能再遇到左边界的max再删,所以左边界j-1,留至少一步,
如果左右都有,都可以删,容斥一下,所有的减去都是j的(一个也删不了)。
初始化注意一下 O(nlog^2n)
const int N=100;
ll dp[1010][1010][2][2],mod,C[1010][1010];
int n,m;
int main()
{
n=re(),m=re(),mod=re();
_f(i,0,m)dp[0][i][1][0]=dp[0][i][0][1]=dp[0][i][1][1]=dp[0][i][0][0]=1;
C[0][0]=C[1][0]=C[1][1]=1;
_f(i,2,n)
_f(j,0,n)C[i][j]=(C[i-1][j]+((j>0)?(C[i-1][j-1]):(0)))%mod;
_f(i,1,n)//长度
_f(j,1,m)//最多的操作次数
_f(k,1,i)//左右区间分别的长度
{
dp[i][j][0][0]+=dp[k-1][j][0][1]*dp[i-k][j][1][0]%mod*C[i-1][k-1]%mod;
dp[i][j][0][0]%=mod;
dp[i][j][1][0]+=dp[k-1][j-1][1][1]*dp[i-k][j][1][0]%mod*C[i-1][k-1]%mod;
dp[i][j][1][0]%=mod;
dp[i][j][0][1]+=dp[k-1][j][0][1]*dp[i-k][j-1][1][1]%mod*C[i-1][k-1]%mod;
dp[i][j][0][1]%=mod;
dp[i][j][1][1]+=(dp[k-1][j][1][1]*dp[i-k][j][1][1]%mod+mod-(dp[k-1][j][1][1]-dp[k-1][j-1][1][1]+mod)*(dp[i-k][j][1][1]+mod-dp[i-k][j-1][1][1])%mod)*C[i-1][k-1]%mod;
dp[i][j][1][1]%=mod;
}
chu("%lld",(dp[n][m][0][0]-dp[n][m-1][0][0]+mod)%mod);
return 0;
}
T3:给你一个有向图,每个节点有点权,问你一种遍历方法,从1到n,再从n到1,经过路径上的点权加到ans里,但是每个点###的点权只能加一次,问你
最小的ans是多少
其实问题就在于怎么去表示跑过来又跑回去的过程中,我每一个决策对应的选的点不同,那可以免费的点也不同,很难记录完全
怎么解决?其实就是我选择一种玄学的算法,决定了我当前就是最优解,这样只记录当前的唯一状态然后跑下去就行
(1)瞎搞:从正跑到反,删点,然后从反跑到正,记录ans1;默认2号点必须选,点权0,正反跑dij,反正跑dij(第一遍就边跑边珊,第二遍跑回去)
这样跑n-2次,最后答案从(n-2)2+1个答案里选
O(n^2m)
()正解:我建立2张图,建立2个最短路状态,同时跑dij,但是共用一个遍历到的点的记录数组。
bitset g[i][j]记录正着走到i,反着走到j的经过的节点是谁
dis[i][j]记录我收获了多少ans,然后跑的时候就一块跑,非常神奇而且快速....O(nlogn)
const int N=100;
struct node
{
int to,nxt;
}e[200000];
int tot,head[260][2],n,m;
bitset<260>g[260][260];bool vis[260][260];
int dis[260][260],a[260];
struct stu
{
int x,y,ds;
bool operator<(const stu&A)const
{
return ds>A.ds;
}
stu(){}
stu(int xxx,int yyy,int dsds)
{
x=xxx,y=yyy,ds=dsds;
}
}lik;
priority_queue<stu>st;
inline void Add(int x,int y,int id)
{
e[++tot].nxt=head[x][id],e[tot].to=y;head[x][id]=tot;
}
inline void Dijstra()
{
memset(dis,0x3f,sizeof(dis));
lik.x=1,lik.y=n,lik.ds=a[1]+a[n];st.push(lik);
g[1][n][1]=1;g[1][n][n]=1;
while(!st.empty())
{
lik=st.top();st.pop();
vis[lik.x][lik.y]=1;
for(rint i=head[lik.x][0];i;i=e[i].nxt)
{
int to=e[i].to,redis=lik.ds;
if(!g[lik.x][lik.y][to])redis+=a[to];
if(dis[to][lik.y]>redis)
{
dis[to][lik.y]=redis;
g[to][lik.y]=g[lik.x][lik.y];
g[to][lik.y][to]=1;
st.push(stu(to,lik.y,redis));
}
}
for(rint i=head[lik.y][1];i;i=e[i].nxt)
{
int to=e[i].to,redis=lik.ds;//价值和
if(!g[lik.x][lik.y][to])redis+=a[to];
if(dis[lik.x][to]>redis)
{
dis[lik.x][to]=redis;
g[lik.x][to]=g[lik.x][lik.y];
g[lik.x][to][to]=1;
st.push(stu(lik.x,to,redis));
}
}
while(!st.empty()&&vis[st.top().x][st.top().y])st.pop();
}
}
int main()
{
n=re(),m=re();
_f(i,1,n)a[i]=re();
_f(i,1,m)
{
int ai=re(),bi=re();
Add(ai,bi,0);Add(ai,bi,1);
}
Dijstra();
if(dis[n][1]<0x3f3f3f3f)chu("%d",dis[n][1]);
else chu("-1");
return 0;
}
T4:给你若干个二维矩形的坐标,求这若干个矩形相交形成的联通块个数(n<1e5
正常思路:按照左端点排序,每次左边加边,右边查边,查满足纵向相交而且左边加入的边的右边界<=加的右边。这个可以用线段树维护。mark:代表当前区间是否是被连续区间覆盖(就是左儿子右儿子没有分别递归下去的不同颜色);id:当前区间如果是唯一完全覆盖,就是那个完全覆盖的编号,否则无效;max_x:当前完全覆盖的颜色的右边界,因为我只是+左,查右,左边不会删除,所以用一个边界时刻限制和删除不合法的颜色合并;tag:完全覆盖的懒度标记。最妙的一处是,我怎么解决的覆盖和连接问题,就是假如(2,4)区间被x覆盖,但是(1,1)和(3,4)分别被不同覆盖,而且右边界更大,这样我就没办法正确更新了,所以我规定无论是修改还是查询,必须在唯一颜色唯一覆盖的时候进行。最关键的是pushup和pushdown
pushup:如果左边和右边颜色一样就让mark=1,id更新,如果不一样mark=2,id无效;但是左右区间的id也可能无效,所以还要mark[lson][rson]判断一下。
pushdown:懒度标记,id和mark都更新一下,因为唯一覆盖了,所以所有零碎的区间都连在一起可以忽略了,那么max_x就取max就行
const int N=1e5+10;
int n,fa[N];
struct Rectangle{
int r1,c1,r2,c2;
bool operator<(const Rectangle&G)const
{
return r1<G.r1;
}
}rec[N];
inline int Find(int ro)
{
if(ro==fa[ro])return ro;
return fa[ro]=Find(fa[ro]);
}
struct Segtree{
int ls[N<<2],rs[N<<2],tot,max_x[N<<2],id[N<<2],tag[N<<2],mark[N<<2],root;
Segtree(){
memset(ls,0,sizeof(ls));
memset(rs,0,sizeof(rs));
memset(max_x,0,sizeof(max_x));
memset(id,0,sizeof(id));
memset(tag,0,sizeof(tag));
memset(mark,0,sizeof(mark));tot=root=0;
}
inline void Build(int&rt,int l,int r){
if(!rt)rt=++tot;
mark[rt]=1;
if(l==r)return;
int mid=(l+r)>>1;
Build(ls[rt],l,mid);Build(rs[rt],mid+1,r);
}
inline void Pushup(int rt)
{
if(id[ls[rt]]==id[rs[rt]]){
max_x[rt]=max(max_x[ls[rt]],max_x[rt]);id[rt]=id[ls[rt]];
mark[rt]=1;
//如果是自己本身还有颜色,就不是唯一颜色覆盖了
//但是一定是全的,所以互相覆盖等价一个,max_x取max就行
}
else mark[rt]=2;//否则就是多颜色覆盖或者无颜色覆盖,就是无效
if(mark[ls[rt]]==2||mark[rs[rt]]==2)mark[rt]=2;
}
inline void Pushdown(int rt)
{
if(tag[rt]){
tag[ls[rt]]=tag[rs[rt]]=1;tag[rt]=0;
max_x[ls[rt]]=max_x[rs[rt]]=max_x[rt];id[ls[rt]]=id[rs[rt]]=id[rt];//完全覆盖只代表本层,mark不能改
}//完全覆盖,那么一定都连着,所以mark不用改,就当是一个,在这一层起作用就行
}
inline void Update1(int rt,int l,int r,int L,int R,int x,int k)//L,R的上下界,x是最右端,k是编号
{
if(L<=l&&r<=R&&mark[rt]==1){
if(max_x[rt]<x)return;
fa[Find(k)]=Find(id[rt]);return;
}
if(l==r)return;
//这里的mark代表的不是唯一覆盖,是子区间有没有部分不相交的覆盖
int mid=(l+r)>>1;
Pushdown(rt);
if(L<=mid)Update1(ls[rt],l,mid,L,R,x,k);
if(R>mid)Update1(rs[rt],mid+1,r,L,R,x,k);
}
inline void Update2(int rt,int l,int r,int L,int R,int x,int k)
{
if(L<=l&&r<=R&&mark[rt]==1){//这里不是不合法,是一定和之前矩阵交而且右边界没有贡献,直接忽略
//如果忽略标记就不要瞎穿,因为之前pushdown可能已经解锁了标记
if(x<max_x[rt])return;
max_x[rt]=x;id[rt]=k;tag[rt]=1;return;
}
if(l==r)return;
Pushdown(rt);
int mid=(l+r)>>1;
if(L<=mid)Update2(ls[rt],l,mid,L,R,x,k);
if(R>mid)Update2(rs[rt],mid+1,r,L,R,x,k);
Pushup(rt);
}
}T;
int main()
{
n=re();
int mx_c=0;
_f(i,1,n){
fa[i]=i;
rec[i].r1=re(),rec[i].c1=re(),rec[i].r2=re(),rec[i].c2=re();
mx_c=max(mx_c,rec[i].c2);
}
sort(rec+1,rec+1+n);T.root=T.tot=0;
T.Build(T.root,1,mx_c);
_f(i,1,n){
T.Update1(T.root,1,mx_c,rec[i].c1,rec[i].c2,rec[i].r1,i);
T.Update2(T.root,1,mx_c,rec[i].c1,rec[i].c2,rec[i].r2,i);
}
int cnt=0;
_f(i,1,n)if(fa[i]==i)cnt++;
chu("%d",cnt);
return 0;
}