每日一(水)题
索引:
牛客网:数学考试(前缀和)
牛客网:与众不同(线段树/ST表 + 贪心)
牛客网:位运算(线段树)
CF1092F Tree with Maximum Cost(换根DP)
最大子段和
CF22C[System Administrator]
[APIO2010]特别行动队(斜率优化dp)
CF822C Hacker, pack your bags!(还不错的思维题)
上帝造题的七分钟[花神游历各花园]
CSP爆炸了,来刷水题改善心情........
T1:牛客网:数学考试
Day 1 : 11.08
给定一个序列,选出两段长度为\(k\)的不重合序列使得和最大
即\([L,L+1,L+2,....,L+k-1]\)+\([R,R+1,R+2,...,R+k-1]\)的和最大(R >= L+k)。
直接预处理前缀和即可,因为长度是固定的,枚举\(R\)点,然后\(L\)就固定了是\(1\)到\(R - k\)(R要从\(k + 1\)开始枚举)然后用ST表维护最大值啥的就阔以了.(其实可以O(1)找最大值并且不需要预处理,因为我的\(R\)不停右移,实际上只会填入一个左端点)
Code:
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int MAXN = 200005;
int n;
int T,k;
int a[MAXN];
int sum[MAXN],New[MAXN];
int Max ;
signed main()
{
cin >> T;
while(T)
{
int ans = -100000000000;
cin >> n >> k;
for(int i = 1 ; i <= n ; i ++)
{
cin >> a[i];
sum[i] = sum[i - 1] + a[i];
}
int len = 0;
for(int i = 1 ; i <= n ; i ++)
{
if(i + k - 1 <= n)
New[i] = sum[i + k - 1] - sum[i - 1],len = i;
}
Max = -10000000000;//之前我这里没改成这么小就一直Wa一个点.....
for(int R = k + 1 ; R <= len ; R ++)
Max = max(Max,New[R - k]),ans = max(New[R] + Max,ans);
cout << ans << endl;
T --;
}
return 0;
}
题目的变形很有意思:
来自牛客网王清楚
不限制长度——在一个数列里找两个不相交区间使得他们权值和最大
对于变形,我们可以想到想到找一个"分界线",显然我们找的两个区间不能跨过这一条分界线,不妨假设这条分界线为\(K\),使得左边的区间可以包含这条分界线,但是右边的不能(这样子保证不重合)
那么问题就变成了在区间\([1,K]\)以及区间\([K + 1 , n]\)找两个最大子段和即可,貌似线段树是可做的?时间复杂度\(n(log(n))\)?为什么数据给的那么小......。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int MAXN = 5005;
int n,Ans = -1000000000;
int a[MAXN];
struct Segment {
int l,r;
int lmax,rmax,dat,sum;
} T[MAXN * 4];
void updata(int x)
{
T[x].sum = T[x << 1].sum + T[x << 1 | 1].sum;
T[x].lmax = max(T[x << 1].sum + T[x << 1 | 1].lmax, T[x << 1].lmax);
T[x].rmax = max(T[x << 1 | 1].sum + T[x << 1].rmax , T[x << 1 | 1].rmax);
T[x].dat = max(max(T[x << 1].rmax + T[x << 1 | 1].lmax,T[x << 1].dat),T[x << 1 | 1].dat);
return ;
}
void build(int x,int l,int r)
{
T[x].l = l , T[x].r = r;
if(l == r)
{
T[x].lmax = T[x].rmax = T[x].dat = T[x].sum = a[l];
return ;
}
int mid = (l + r) >> 1;//常规建树.
build(x << 1 , l , mid);
build(x << 1 | 1 , mid + 1 , r );
updata(x);
return ;
}
Segment GetMax(int x,int l,int r)//线段树实现区间查询最大子段和
{
if(T[x].l >= l && T[x].r <= r)return T[x];
int mid = (T[x].l + T[x].r) >> 1;
if(l > mid)return GetMax(x << 1 | 1 , l , r);//如果l大于mid,答案在右边
if(r <= mid)return GetMax(x << 1 , l , r);//r 小于等于mid,答案在左边
Segment A = GetMax(x << 1 , l , mid),B = GetMax(x << 1 | 1 , mid + 1 , r);
Segment ans;
ans.sum = A.sum + B.sum;
ans.lmax = max(A.lmax , A.sum + B.lmax);
ans.rmax = max(B.rmax , B.sum + A.rmax);
ans.dat = max(max(A.rmax + B.lmax , A.dat) , B.dat);//合并信息即可
return ans;
}
signed main()
{
cin >> n;
for(int i = 1 ; i <= n ; i ++)cin >> a[i];
build(1,1,n);
for(int i = 1 ; i < n ; i ++)//枚举断点
Ans = max(GetMax(1,1,i).dat + GetMax(1,i + 1 , n).dat,Ans);//查询答案即可
cout << Ans << endl;
return 0;
}
上面是我的解法。
官方的解法点击上面的链接即可!
好了这道题就水到这里..........
Get_Top
T2:牛客网:与众不同
Day 2 : 11.09
题意简化:
给定一个长度为\(n\)的序列,m个询问,每次询问一个区间\([L,R]\)内最长的没有重复数字的子序列,\(n\)以及\(m\) <=
\(2 * 10^5\)
样例:
9 2
2 5 4 1 2 3 6 2 4
0 8
2 6
output:
6
5
思路一:
有手就行的暴力,枚举\([L,R]\)区间内的子序列,然后O\((len)\)扫一遍,总的时间复杂度是O\((n^3 * m)\)
思路二:
借助线段树,枚举子区间,\(O(log2(len))判断\),总的时间复杂度是:O\((n ^ 2 log n * m)\)
显然上面两个都是错误的,但是第二个貌似时间上更优秀......
继续想!
思路三:
二分最长的长度,对于判断,每一次枚举左端点,那么右端点便是固定的,然后借助线段树O\((n log^2(n) * m )\)
上面这个解法已经比思路一和二优秀很多了。但是还是过不掉这道题,怎么办?
继续思考。
思路四:
记录每一个元素的前一个与它相同的元素的位置是哪一个。
我们发现,对于一个左端点来说,其最长的,没有重复数字的子序列的长度是固定的。
固定左端点后,我们总是希望不停的往右边挪动右端点以求能够有一个最长的子序列。
实际上,我们对于每一个左端点,不停移动右端点(这个右端点只会增加而不会减小,你不妨手玩几组,想一想,为什么),总的时间复杂度O(\(n\)),可行,动手写代码。
对于每一个点\(i\),当它为左端点时不妨设其最长没有重复元素的子段的右端点为:\(r[i]\)
同时,我们观察到,得到的\(R\)序列是单调不降的,于是就方便处理,用二分.
对于一个查询怎么办?
\(区间[L,R]之间的任意一个点作为左端点\)
-
Case 1 左端点对应的最长的没有重复元素的子序列的右端点大于\(R\),
显然这些左端点只有最左边的那个是"有用"的,同时因为得到的\(r[]\)是单调不降的,我们很容易用二分找到这一个最左边的\(r[i]\)大于\(R\)的点。
-
Case2 左端点对应的最长的没有重复元素的子序列的右端点小于等于\(R\)的,
取最大的\(r[i]-i\)(预处理出\(r[i]-i\)),ST表 \(or\) 线段树维护即可,但是这里显然用ST表更优。
比较上下两种情况即可
总的时间复杂度为:O\(((m + n)logn)\)
Code :
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005 , MAXM = 200005;
int n , m,M ,flag,tot = 1;
int a[MAXN],Last[MAXN],book[MAXN],R[MAXN];
int T[MAXN],Max[MAXN][25];
struct Node {
int data,id;
} P[MAXN];
void Prepare();
int cmp(Node A, Node B);
int GetMax(int l,int r)
{
if(l > r)return 0;
int k = log2(r - l + 1);
return max(Max[l][k] , Max[r - (1 << k) + 1][k]);
}
int main()
{
cin >> n >> m;
for(int i = 1 ; i <= n ; i ++)
cin >> a[i],P[i].id = i , P[i].data = a[i];
Prepare();//离散化等预处理
for(int i = 1 ; i <= n ; i ++)
{
int S = R[i - 1];
while(S + 1 <= n)
{
if(i <= Last[S + 1] && Last[S + 1] <= S)break;
S ++;//只增不降
}
R[i] = S;
T[i] = R[i] - i + 1;
Max[i][0] = T[i];
}
for(int j = 1 ; j <= log2(n) ; j ++)
for(int i = 1 ; i + (1 << j) - 1 <= n ; i ++)
Max[i][j] = max(Max[i][j - 1] , Max[i + (1 << (j - 1))][j - 1]);
for(int v = 1 ; v <= m ; v ++)
{
int l,r;
cin >> l >> r;
l ++ , r ++;
int S = r,Ans = -1;
for(int i = log2(r - l + 1) + 1; i >= 0; i --)
if(S - (1 << i) >= l && R[S - (1 << i)] > r)S = S - (1 << i);//二分我换成了倍增
Ans = r - S + 1;//Case 1
Ans = max(Ans,GetMax(l,S - 1));
cout << Ans << endl;
}
return 0;
}
void Prepare()
{
sort(P + 1 , P + 1 + n , cmp);
for(int i = 1 , now = 1; i <= n ; i ++)
{
if(P[i].data != P[i - 1].data)now ++;
a[P[i].id] = now;
}//离散化
R[0] = 1;
for(int i = 1 ; i <= n ; i ++)
{
Last[i] = book[a[i]];
book[a[i]] = i;
M = max(Last[i],M);
}//找last[i]
return ;
}
int cmp(Node A , Node B)
{
return A.data < B.data;
}
\(Don't\) \(stop\) \(thinking\) . \(The\) \(person\) \(who\) \(stops\) \(thinking\) \(will\) \(never\) \(be\) \(successful.\)
好了这题就完了.
Get_Top
牛客网:位运算
Day 3 : 11.10
题意简化:
一行表示一个操作。所有操作形如 opt l r v。
\(opt=1 表示将区间[l,r]循环右移v位\)( 不是区间位移,是区间中的每一个数的二进制位的位移)
\(opt=2 表示将区间[l,r]循环左移v位\)( 不是区间位移,是区间中的每一个数的二进制位的位移)
\(opt=3 表示将区间[l,r]按位或上v\)
\(opt=4 表示将区间[l,r]按位与上v\)
\(opt=5 询问区间[l,r]的和\)
注意:为了优化你的做题体验,操作5也会输入一个v,但是是没有意义的。
对于00000000000000000101,右移一位后会变成10000000000000000010,左移的情况同理
Get_Top
\(1 ≤ N,Q ≤ 2*10^5 0 ≤ ai < 2^{20}\)
空间:256MB
时间:3s
水话:
感觉比较有趣,这个线段树应该写起来没有看起来那么阴间.......应该吧,应该,吧。
这个题比较创新了,给的输入也很好啊,特别是那个\(opt 5\)给的特殊照顾。
考虑怎么做。
思路:
这道题给的空间以及时间不一般。
于是我开了一个这样的玩意:
struct Segment {
int l,r,query[21];
bitset <21> booka;
bitset <21> bookb;
int lef,rig;
bool flaga,flagb;
} T[MAXN * 4];
不妨考虑开一棵线段树,每一个节点记录区间\([L,R]\)下的情况,而\(query[i]\)则记录了区间\([L,R]\)之间所有数有多少个第\(i\)位上为1。
一共是\((MAXN * 84)\),\(1.7 * 10^7的int数组,大概60多MB\),显然可以暴力开!
算一算需要用的时间,大概是O\((mlog(n)*20*线段树常数)\),感觉很危!危!危!(其实是\(10^8\)多一点点,要是它把时限开到1s,那我就,我就,我就,鲨了出题人,但是这个时限显然能过
不管了。我就是喜欢暴力!
考虑怎么维护。
线段树最重要的是什么?告诉我!!!
卡常!卡常!卡常!啪,我一个大耳巴子就是飞过去。
线段树我们首先要考虑标记的传递,这是最重要的,也是线段树的灵魂,同等重要的就是对于区间信息的合并。(个人觉得
考虑传递标记的顺序
我们发现,或以及与运算的优先级是在左移右移之后的,而左移右移其实是同级而且可以互相抵消的。
举个栗子:我左移\(k\)位,右移\(j\)位,不妨令\(k\) > \(j\)实际上就是左移\(k - j位\)
考虑或运算有什么特别的地方:
假如给定的\(v\)二进制下第\(i\)位是\(1\),相当于一个区间赋值,区间\([l,r]\)内的数字的第\(i\)位都要变成\(1\)
对于\(v\)二进制下为\(0\)的位数,我们根本不用管,没有什么影响
考虑与运算有什么特别的地方:
假如给定的\(v\)二进制下第\(i\)位为\(0\),相当于区间赋值,区间\([l,r]\)内的数字第\(i\)位显然都要变为\(0\),
对于为\(v\)二进制下为\(1\)的位数,不用考虑。
显然无论是或还是与,都要在左移右移后完成,但是或以及与,则是讲究先来后到的顺序。
这就是传递的顺序。
怎么求和?
这个,根据我保存的东西你就可以很快\(Get\)到我的\(mean\)了吧。
每一次操作的时间复杂度都是O\((log(n)*20 * 线段树常数)\)的,所以总的时间复杂度就O(\(m * log(n)*20 * 线段树常数\))
CF1092F Tree with Maximum Cost
Day 4. 11.11(双十一)
题意:
- 给定一棵\(n\) 个节点的树,每个节点带有点权\(a_i\)
- 定义\(dist(u,v)为u,v两个点的距离(每条边长度为1)\)
- 请你选一个点\(u\),最大化:
\(\sum_{i = 1}^{i = n}{dis(u,i)*a_i}\)
现在问你最大化的\(\sum_{i = 1}^{i = n}{dis(u,i)*a_i}\)为多少。
\(1 <= n,a_i <= 2*10^5\)
\(input:\)
8
9 4 1 7 10 1 6 5
1 2
2 3
1 4
1 5
5 6
5 7
5 8
\(output:\)
121
样例解释:人工算出来应该是选择3号点得到答案的(确 信
思路:
思路一:
WTM直接暴力乱搞,O(\(n\))枚举所有点,跑一遍DFS计算每个点的距离,再暴力的去计算答案,时间复杂度O(\(n^2\))
思路二:
考虑贪心,但是感觉这个题目不是那么好贪心的样子........有哪个神想出了贪心的话,叫我一声(感觉貌似不能贪心?
思路三:
感觉思路一可以优化的样子,因为它的答案正确性貌似是毋庸置疑的,这比这个玄学贪心好多了,但是时间上貌似是不对的......
考虑如何优化。假设现在我们确定了一点\(u\)为选定的答案节点,获得的答案是\(Now\),假如我们把这个选定的点换成与它相邻的点\(p\)会怎么样?
显然,对于\(u\)为根的时候,子树\(p\)内的所有点,整个的贡献就会减少1,同时,原来以\(u\)为根的时候,不在子树\(p\)中的点的贡献都整体加了1。
于是,主角出来了:\(换根dp\)!
这玩意感觉就是暴力,为什么叫\(dp(大雾\),我们先确定以1为根的时候的答案(即取1为那一个用来计算答案的\(u\),同时预处理出每个“子树的大小”
- 规定\(siz[v]\)为子树\(v\)的“子树的大小”
这里的“子树的大小”指的是每个子树中所有点的点权和,每次我们换一个根(注意,这里只能换成与当前根之间有边相连接的边!不然不好计算答案!)
然后每次我们将根移动到相邻的点,目前的\(Now\) = \(Now -siz[v] + (siz[1] - siz[v])\)因为\(siz[v]即是原来在子树v中的所有点的点权和,siz[1]-siz[v]就是不在子树v中的所有点的点权和\)
然后从1开始进行换根即可(不要反复横跳)
\((to != fa[x])\)
于是这东西就叫做"换根\(dp\)"了,又见识到了奇怪的\(dp\)
#include <cstdio>
#include <iostream>
using namespace std;
#define int long long
const int MAXN = 2e5 + 500;
int n,cnt = 0;
int a[MAXN];
int start[MAXN];
int siz[MAXN],fa[MAXN],deep[MAXN],Now = 0,Ans = -1;
struct Edge {
int next,to;
} edge[MAXN * 2];
inline int read()
{
int x = 0, flag = 1;
char ch = getchar();
for( ; ch > '9' || ch < '0' ; ch = getchar());
for( ; ch >= '0' && ch <= '9'; ch = getchar())x = (x << 3) + (x << 1) + ch - '0';
return x * flag;
}
void add(int from,int to)
{
cnt ++;
edge[cnt].to = to;
edge[cnt].next = start[from];
start[from] = cnt;
return ;
}
int DFS1(int x,int from)
{
siz[x] = a[x];
fa[x] = from;//这个玩意我好像没有用到......
deep[x] = deep[from] + 1;//记录一下深度,方便计算初始答案
for(int i = start[x] ; i ; i = edge[i].next)
{
int to = edge[i].to;
if(to != from)siz[x] += DFS1(to,x);//预处理整个子树的大小
}
return siz[x];
}
void Solve(int x,int from)
{
int k = Now;
if(x != 1)
Now += (siz[1] - 2 * siz[x]);
Ans = max(Ans,Now);//统计答案
for(int i = start[x] ; i ; i = edge[i].next)
{
int to = edge[i].to;
if(to == from)continue;
Solve(to,x);//跟相邻的点换根
}
Now = k;
return ;
}
inline void write(int x)
{
if(x == 0){putchar('0');return;}
char b[55];
int R = 0;
while(x)
R ++ , b[R] = x % 10 + '0' , x /= 10;
for(int i = R ; i >= 1 ; i --)
putchar(b[i]);
}
signed main()
{
n = read();
for(int i = 1 ; i <= n; i ++)a[i] = read();
for(int i = 1 ; i <= n - 1 ; i ++)
{
int u , v ;
u = read() , v = read();
add(u,v) , add(v,u);
}
DFS1(1,0);//先来一遍DFS进行预处理
for(int i = 1 ; i <= n ; i ++)
Now += (deep[i] - 1) * a[i];//先预处理出以1节点为答案节点的答案
Solve(1,0);//开始换根
write(Ans);
// ------------------ end -------------------
return 0;
}
(话说我时间复杂度也是O\((n)\)为什么这么卡,跑了32s),怕不是有上千个点.......
ps.
(后来发现原来是我用的cin的缘故,数据点比较多,然后每个数据都有亿点点大,改成快读后变成了2s)
Get_Top
最大子段和
题意
在一个长度为\(n\)的序列里面找一个子段,其长度大于等于\(a\),小于等于\(b\),使得其和最大
Get_TOP
CF22C [System Administrator]
题意:
给定n个顶点,可以在这些顶点中连接m条双向边,要求连接后整个图联通,且去掉v顶点后整个图不连通
若有解,输出所连得m条边 若无解,输出-1 若多解,随意输出一个即可
前言
这是一个构造题。
点明两个公理:
-
一个\(n\)个点的无向完全图最多有\(\frac{(n-1)*n}{2}\)条边。
-
要使得一个\(n\)个点的无向图联通,至少需要\(n-1\)条边
思路:
根据上面提到的公理,我们很明显的可以知道:
-
\(Case1\)
假如给出的\(m\) < \(n-1\),显然无解,输出\(-1\)。
-
\(Case2\)
假若\(m\) = \(n-1\),那么你给出的连通图必然是一棵树,当然,这个题很好满足条件,构建一朵菊花,中心点为\(v\)即可,即用\(v\)向其它\(n-1\)个点连边。
-
\(Case3\)
\(m\) >= \(n\)
从上面\(Case2\)得到启发,我们可以先构造一朵以\(v\)为中心的菊花。
这样子操作后,我们就连上了\(n-1\)个点
我们剩下要连的边数就会等于\(m-n+1\)条。
我们就保证了给出的图一定是联通的。
考虑如何使得整个图按题意删除点\(v\)后整个图不联通。
因为整个图不连通,那么整个图至少分裂出了两个部分。
我们不妨假设其中一个图只有一个点,另外的一个图就会有\(n-2\)个点。
在\(Case3\)下上面情况下会无解?
答案是:当那一个点数为\(n-2\)的图已经是完全图的时候,仍然存在没有连完的边。
因为你把其中点数为\(n-2\)的那一个图已经连满了,那一个图已经无法在自己内部连了,只能连入仅有一个点的那个图了。这样子的话,就算\(v\)点被删除,剩下的整个图仍然会是联通的。
这样子就无解了。也就是\(m - n + 1 < \frac{(n-2)*(n-3)}{2}\)即无解
(下面是关于上面的做法的证明,如果不想看可以选择跳过)
有人会问:为什么一定是两个不连通的图?难道三个的,四个的就不能满足条件了吗?
我们至少得剔除一个点出来,这个点不能与其他点联通(指删除点\(v\)后的图)
剩下的点一共就\(n-2\)个了(因为点\(v\))被删除了 .
证明:当剩下的\(n-2\)个点在同一联通块时,能连的边最多。
假设这\(n-2\)个点分裂成了\(k\)个联通块。
\(k\)个联通块的点数分别为\(p_1,p_2,p_3...p_k\)
并且满足:\(p_1+p_2+p_3+...+p_k = n - 2\)
能连的边的总数就会是:
\(\frac{(p_1-1)*p_1}{2} + \frac{(p_2-1)*p_2}{2}\) + ... + \(\frac{(p_k-1)*p_k}{2}\)
把式子暴力展开变为:
\(\frac{p_1^2}{2} + \frac{p_2^2}{2} + ... +\frac{p_k^2}{2} - \frac{\sum_{i = 1}^{i = k} p_i}{2}\)
把证明的内容写为不等式即为:
\(\frac{p_1^2}{2} + \frac{p_2^2}{2} + ... +\frac{p_k^2}{2} - \frac{\sum_{i = 1}^{i = k} p_i}{2} < \frac{(n-2)(n-3)}{2}\)
不妨将\(p_1 + p_2 + ... + p_k\)写成\(n-2\)的形式。
原式等于:
\(\frac{p_1^2}{2} + \frac{p_2^2}{2} + ... +\frac{p_k^2}{2} - \frac{n-2}{2} < \frac{(n-2)(n-3)}{2}\)
移项,合并同类项:
\(\frac{p_1^2}{2} + \frac{p_2^2}{2} + ... +\frac{p_k^2}{2} < \frac{(n-2)^2}{2}\)
∵ \(p_1+p_2+p_3+...+p_k = n - 2\)
∴ 上面不等式恒成立 。
证毕。
使用完全图的形式使得一个点的度数尽可能多进行连边即可,这个贪心是很显然的。
把任意一个不是点\(v\)的点拿出来,然后剩下的\(n-2\)个点里面按照贪心使得一个点度数尽可能多,任意连\(m-n+1\)条边即可
(继续任意找一个点当做菊花的中心然后就可以递归处理问题了。这是这个题目递归标签的由来)。
这个题目就完了。
Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
int n , m , v;
int arr[100005],tail = 0;
signed main()
{
cin >> n >> m >> v;
if(m < n - 1){cout << -1 ; return 0;}
if(m == n - 1)
{
for(int i = 1 ; i <= n ; i ++)
if(i != v)
cout << v << " " << i << endl;
return 0;
}
m = m - n + 1;
if((n - 2) * (n - 3) / 2 < m){cout << -1 ; return 0;}//判断是否有解
for(int i = 1 ; i <= n ; i ++)
if(i != v)
cout << v << " " << i << endl;//先构造一朵菊花
int now = 0;
while(now <= n - 2)
{
if(now * (now - 1) / 2 >= m)break;
now ++;
}
for(int i = 1 ; i <= n - 1 ; i ++)
{
if(now == 0)break;
if(i != v)tail ++ , now -- , arr[tail] = i;
}
for(int i = 1 ; i < tail && m; i ++)
{
for(int j = i + 1 ; j <= tail && m; j ++)
cout << arr[i] << " " << arr[j] << endl , m --;
}
return 0;
}
Get_Top
[APIO2010]特别行动队
题意:
给你一个长度为\(n\)的序列\(s\),你需要把它进行分段,每一段内的数的编号必须连续。
同时给你一个二次函数解析式,以\(a,b,c\)的形式给出。
每一段的贡献为(假设这一段为\(s_i\),\(s_i + 1\),\(s_i + 2\) ... \(s_{i + k}\)):
\(a*sum^2 + b * sum + c\),\(sum\) = \(\sum {s_i}\)
现在要求你进行分段要求得到的总贡献最大。
做法
最裸的暴力即设立状态\(dp[i]\),表示前\(i\)个数分好段所获得的最大贡献。
状态转移也很容易。
不妨令\(f(x) = ax^2 + bx + c\)
\(dp[i] = min(dp[j] + f(\sum_{k = j + 1}^{k = i}{s[k]}) )(j < i)\)
后面显然可以用前缀和优化。
于是状态转移方程变成了:
\(dp[i] = min(dp[j] + f(sum[i] - sum[j]))(j < i)\)
时间复杂度就变成了O(\(n^2\)),这样子就可以获得\(50pts\)的好成绩了。
考虑使用斜率优化。
拆柿子环节!
\(f(sum[i] - sum[j])\) = \(a*sum[i]^2 + a* sum[j] ^ 2 - 2 * a * sum[i] * sum[j] + b(sum[i] - sum[j]) + c\)
\(f(sum[i] - sum[j])\) = \((a* sum[j] ^ 2 - 2 *a * sum[i] * sum[j] - b * sum[j]) + a*sum[i]^2 + b * sum[i] + c\)
\(f(sum[i] - sum[j])\) = \((a * sum[j] ^ 2 - 2 * a * sum[i] * sum[j] - b * sum[j]) + a*sum[i]^2 + b * sum[i] + c\)
(留坑待填)
Get_Top
CF822C Hacker, pack your bags!
前言
发一篇\(luogu\)上这一题的第一篇题解。
题意:
给定\(n,m\)以及\(n\)个区间,区间\(i\)有左端点\(l_i\)右端点\(r_i\)以及价值\(cost_i\)。要求你选出两个区间,选出的两个区间满足下面两个条件:
-
两个区间没有交集。
-
这两个区间的长度等于\(m\)。
现在要求你选出的两个区间的权值和最小,输出最小权值和。
做法:
因为是两张票,考虑枚举其中一张票。
因为两个票的区间要不相交,所以很容易想到把所有票按照其\(l\)排序,如果\(l\)相等,则按照\(r\)从小到大排序。
通过枚举票\(a\),我们只需要找到所有\(l_b\)大于其\(r_a\)的票,这样子就是不相交。
(不需要考虑\(r\) < 其\(l\)的票,假设满足票\(b\)的\(r\)小于票\(a\)的\(l\),在枚举\(b\)的时候就已经计算过了)
可以通过二分找到第一个\(l\) > \(r_a\)的点。
找\([L,R]\)(所有\(L <= i <= R\)的票\(i\)都满足\(l\)大于票\(a\)的\(r\))内满足\(lenb = x - lena\)的票的最小值。
(\(R\)直接设置为\(n,L\)通过二分找到)
对于枚举的每一个都进行查找很不方便,感觉可以用神奇数据结构来搞(目测线段树?
观察到每次要查的右区间是固定的,不妨将枚举到\(i\)的时候需要维护的\(L\)给记录下来,然后我们把\(L\)按照从大到小排序。
这样子我们O(\(n\))扫一遍过来,如果当前这个点\(i\)上有一个询问\([L,R]\)中\(lenb = x - lena\)的票的最小值:
开个桶在扫的时候记录一下\(Min[lenb]\),也就是长度为\(lenb\)的区间的最小值,这时候对于询问我们就直接获得答案.
\(Talk\) \(is\) \(cheat\),\(show\) \(your\) \(Code\)
Code:
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int MAXN = 400005,INF = 100000000000000000;
int n , m ;
int Min[MAXN],cnt = 0;
struct Node {
int l,r,cost;
} T[MAXN];
struct P {
int L,D,F;
//L记录查[L,R]的左端点L,D表示枚举到的票(区间)的权值,F表示要查询的m-lena
} C[MAXN];
bool cmp(Node A , Node B)
{
if(A.l != B.l)
return A.l < B.l;
return A.r < B.r;
}
int cmp1(P A, P B)
{
return A.L > B.L;
}
inline int read()
{
int x = 0 , flag = 1;
char ch = getchar();
for( ; ch > '9' && ch < '0' ; ch = getchar())if(ch == '-')flag = -1;
for( ; ch >= '0' && ch <= '9' ; ch = getchar())x = (x << 3) + (x << 1) + ch - '0';
return x * flag;
}
signed main()
{
n = read() , m = read();
for(int i = 1 ; i <= n ; i ++)
T[i].l = read(),T[i].r = read(),T[i].cost = read();
sort(T + 1 , T + 1 + n , cmp);
for(int i = 1 ; i <= m ; i ++)Min[i] = INF;
for(int i = 1 ; i <= n ; i ++)
{
int lena = T[i].r - T[i].l + 1;
if(lena >= m){C[i].L = -1;continue;}
//如果当前区间长度大于等于m了,另外一个区间需要长度小于等于0,不存在
int K = n + 1;
for(int j = log2(n - i + 1) ; j >= 0 ; j --)
if(T[K - (1 << j)].l > T[i].r && K - (1 << j) >= 1)K -= (1 << j);//二分换成倍增了。。。
if(K != n + 1)
C[i].L = K , C[i].D = T[i].cost , C[i].F = m - lena;
else C[i].L = -1;
}
sort(C + 1 , C + 1 + n , cmp1);//按照L排序
int now = 1,Ans = INF;
for(int i = n ; i >= 1 ; i --)
{
if(C[now].L == -1)break;//如果没有询问了直接break
int lenb = T[i].r - T[i].l + 1;
Min[lenb] = min(Min[lenb],T[i].cost);//扫的时候记录最小值
while(C[now].L == i && now <= n)
{
if(C[now].L == -1)break;//没有询问了
Ans = min(Ans,C[now].D + Min[C[now].F]);
//一个点可能有多个询问,所以要用while
now ++;
}
}
if(Ans == INF) cout << -1;
else cout << Ans;
return 0;
}
另外建议一定用快读,没开快读,评测得贼慢,虽然不会超时,但是你得评测\(1\)分钟多(因为一共136个点!)
没开快读,评测用时\(1.57\)\(min\),你没有看错,是\(min\)
开了快读:\(11.11s\)
Get_Top
咕咕咕了好久了。决定重新开始写,选的题目类型也有所改变(11.23)
Get_Top
上帝造题的七分钟[花神游历各花园]
前言:
老早之前就打算写的一道题了。好久没写,今天有空,写了一发,发现了写分块的新姿势,记之。
思路
本题要求支持区间开方,实际上\(10^{12}\)以内的数开方6次就全会变为1,那么有一些开方操作就是无用的。
不妨给每一个块一个标记,如果本块内所有元素都变成了1,那么我们就可以不对这个块进行开方,否则的话,就暴力开方,对一整个块都开方。
这样子的话,时间复杂度是可以保证不会高于O(\(n\sqrt{n}\))的。
然后发现了写分块的新姿势:
int GetSum(int l,int r)
{
int Ans = 0;
if(pos[l] == pos[r])
{
for(int i = l ; i <= r ; i ++)Ans += a[i];
return Ans;
}
else
{
Ans += GetSum(l , R[pos[l]]);
Ans += GetSum(L[pos[r]] , r);//新知识
for(int i = pos[l] + 1 ; i <= pos[r] - 1 ; i ++)
Ans += sum[i];
return Ans;
}
}
这个求和操作是不是很清爽?这就是我发现的一个新姿势,就比较省事。
整个代码比较简短:
#include <bits/stdc++.h>
using namespace std;
#define int long long
int n,Q;
int a[100005],sum[1005],tag[1005];
int pos[100005] , L[1005] , R[1005];
void change(int l,int r){
if(pos[l] == pos[r]){
if(tag[pos[l]] == 1)return ;
for(int i = l ; i <= r ; i ++)
a[i] = floor(sqrt(a[i]));
sum[pos[l]] = 0;
int flag = 0;
for(int i = L[pos[l]] ; i <= R[pos[l]] ; i ++){
if(a[i] > 1)flag = 1;
sum[pos[l]] += a[i];
}
if(!flag)tag[pos[l]] = 1;
}
else {
change(l , R[pos[l]]);
change(L[pos[r]] , r);
for(int i = pos[l] + 1 ; i <= pos[r] - 1 ; i ++){
if(tag[i])continue;
else change(L[i],R[i]);
}
}
return ;
}
int GetSum(int l,int r){
int Ans = 0;
if(pos[l] == pos[r]){
for(int i = l ; i <= r ; i ++)Ans += a[i];
return Ans;
}
else {
Ans += GetSum(l , R[pos[l]]);
Ans += GetSum(L[pos[r]] , r);
for(int i = pos[l] + 1 ; i <= pos[r] - 1 ; i ++)
Ans += sum[i];
return Ans;
}
}
//预处理分块
void prepare(){
int block = sqrt(n),T = 0;
while(R[T] != n){
T ++ ;
L[T] = (T - 1) * block + 1;
R[T] = min(T * block , n);
for(int i = L[T] ; i <= R[T] ; i ++)
pos[i] = T,sum[T] += a[i];
}
return ;
}
signed main(){
cin >> n;
for(int i = 1 ; i <= n ; i ++)cin >> a[i];
prepare();
cin >> Q;
for(int i = 1 ; i <= Q ; i ++){
int op,l,r;
cin >> op >> l >> r;
if(l > r)swap(l,r);
if(op == 0)change(l,r);
else cout << GetSum(l,r) << endl;
}
return 0;
}