突然更新
0916
FJOI2015火星商店问题
sol 比较显然地这道题能够想到可持久化Trie,我们对于特殊商品直接上可持久化Trie统计答案。对于加入的商品和查询操作,我们维护一棵线段树,每棵线段树维护下标在这段范围内的所有数的Trie,对于Trie上的每一个节点同时维护修改时间的最大值,这样一次询问就可以对应到线段树上log个Trie然后询问即可。时空复杂度$O(nlog^2n)$。
空间复杂度有点高考虑优化。注意到没有强制在线,我们可以在上面的操作基础上将所有操作和询问离线挂在对应的线段树节点上,然后对于线段树上的每一个节点用Trie做一遍。这样空间复杂度就是$O(nlogn)$的。还可以以时间为线段树下标离线,即线段树分治,基本思路也是相同的。
空间复杂度有点高考虑优化。注意到没有强制在线,我们可以在上面的操作基础上将所有操作和询问离线挂在对应的线段树节点上,然后对于线段树上的每一个节点用Trie做一遍。这样空间复杂度就是$O(nlogn)$的。还可以以时间为线段树下标离线,即线段树分治,基本思路也是相同的。
#include
using namespace std;
int read(){
int a = 0; char c = getchar();
while(!isdigit(c)) c = getchar();
while(isdigit(c)){a = a * 10 + c - 48; c = getchar();}
return a;
}
const int _ = 1e5 + 7;
namespace Trie{
const int __ = _ * 40;
int ch[__][2] , lst[__] , cnt;
int create(int x){int t = ++cnt; ch[t][0] = ch[x][0]; ch[t][1] = ch[x][1]; lst[t] = lst[x]; return t;}
void ins(int &rt , int t , int x){
rt = create(rt); int cur = rt;
for(int i = 17 ; i >= 0 ; --i){
bool flg = x >> i & 1; lst[cur = ch[cur][flg] = create(ch[cur][flg])] = t;
}
}
int qry(int rt , int x , int t){
int sum = 0;
for(int i = 17 ; i >= 0 ; --i){
bool flg = !(x >> i & 1);
if(lst[ch[rt][flg]] >= t){sum |= 1 << i; rt = ch[rt][flg];}
else rt = ch[rt][!flg];
}
return sum;
}
}
struct query{int L , R , val , id;};
struct modify{int plc , num;};
int N , M , Q , cnt , ans[_] , rt[_];
namespace segt{
vector < query > qry[_ << 2]; vector < modify > mdy[_ << 2];
#define mid ((l + r) >> 1)
#define lch (x << 1)
#define rch (x << 1 | 1)
void ins(int x , int l , int r , int L , int R , query q){
if(l >= L && r <= R) return qry[x].push_back(q);
if(mid >= L) ins(lch , l , mid , L , R , q);
if(mid < R) ins(rch , mid + 1 , r , L , R , q);
}
void ins(int x , int l , int r , int tar , modify m){
mdy[x].push_back(m); if(l == r) return;
mid >= tar ? ins(lch , l , mid , tar , m) : ins(rch , mid + 1 , r , tar , m);
}
void work(int x , int l , int r){
Trie::cnt = 0; int rt = 0 , pos = 0;
sort(mdy[x].begin() , mdy[x].end() , [&](modify A , modify B){return A.plc < B.plc;});
sort(qry[x].begin() , qry[x].end() , [&](query A , query B){return A.R < B.R;});
mdy[x].push_back((modify){(int)1e9 , 0});
for(auto t : mdy[x]){
while(pos < qry[x].size() && qry[x][pos].R < t.plc){
if(qry[x][pos].id == 168)
qry[x][pos].id = 168;
ans[qry[x][pos].id] = max(ans[qry[x][pos].id] , Trie::qry(rt , qry[x][pos].val , qry[x][pos].L));
++pos;
} Trie::ins(rt , t.plc , t.num);
}
if(l != r){work(lch , l , mid); work(rch , mid + 1 , r);}
}
}
int main(){
#ifndef ONLINE_JUDGE
freopen("in","r",stdin);
freopen("out","w",stdout);
#endif
N = read(); M = read(); rt[0] = 0;
for(int i = 1 ; i <= N ; ++i) Trie::ins(rt[i] = rt[i - 1] , i , read());
for(int i = 1 ; i <= M ; ++i)
if(read()){
int L = read() , R = read() , x = read() , d = read() , id = ++Q;
if(d) segt::ins(1 , 0 , M , max(cnt - d + 1 , 0) , cnt , (query){L , R , x , id});
ans[id] = max(ans[id] , Trie::qry(rt[R] , x , L));
}
else{
int tms = ++cnt , plc = read() , val = read();
segt::ins(1 , 0 , M , tms , (modify){plc , val});
}
segt::work(1 , 0 , M); for(int i = 1 ; i <= Q ; ++i) printf("%d\n" , ans[i]);
return 0;
}
nowcoder186E
sol 一个显然的暴力是对于每一条链设$f_{i,j}$表示考虑到从起点到$i$的路径选择了$\mod K = j$的子集的方案数,转移是一个背包。注意到这是一个链上的问题,考虑用树剖解决,但是两个DP数组直接合并是$O(k^2)$的,这样复杂度就会变成$O(qk^2logn)$还是TLE。
上面的事情告诉我们:两条链的合并的复杂度似乎并没有由一条链向外拓展来得优。那么我们考虑以尽可能少的遍历次数来拓展出所有的路径。不难考虑到点分治,因为点分治的过程中就有很多的暴力遍历的过程。
我们对于每一个点用链表挂上路径端点为它的所有询问,在点分治的时候如果两个位置都计算到了答案就进行合并。似乎这里复杂度变成了$O(nklogn+qk^2)$,后面的$qk^2$还是比较大。但是我们最后的合并实际上只需要合并之后的第$0$项的值,所以我们可以$O(K)$去计算它。这样复杂度就是$O(nklogn+qk)$,可以通过。
上面的事情告诉我们:两条链的合并的复杂度似乎并没有由一条链向外拓展来得优。那么我们考虑以尽可能少的遍历次数来拓展出所有的路径。不难考虑到点分治,因为点分治的过程中就有很多的暴力遍历的过程。
我们对于每一个点用链表挂上路径端点为它的所有询问,在点分治的时候如果两个位置都计算到了答案就进行合并。似乎这里复杂度变成了$O(nklogn+qk^2)$,后面的$qk^2$还是比较大。但是我们最后的合并实际上只需要合并之后的第$0$项的值,所以我们可以$O(K)$去计算它。这样复杂度就是$O(nklogn+qk)$,可以通过。
#include
using namespace std;
int read(){
int a = 0; char c = getchar();
while(!isdigit(c)) c = getchar();
while(isdigit(c)){a = a * 10 + c - 48; c = getchar();}
return a;
}
const int _ = 2e5 + 7 , MOD = 998244353;
vector < int > ch[_] , qry[_];
int N , K , Q , l[_] , r[_] , ans[_] , val[_];
struct mean{int arr[50]; mean(){memset(arr , 0 , sizeof(arr));} int& operator [](int x){return arr[x];}}now[_];
mean unit(){mean t; t[0] = 1; return t;}
mean add(mean p , int q){mean tmp = p; for(int i = 0 ; i < K ; ++i) tmp[(i + q) % K] = (tmp[(i + q) % K] + p[i]) % MOD; return tmp;}
bool vis[_] , in[_]; int nsz , msz , mid;
void getsz(int x){++nsz; vis[x] = 1; for(auto t : ch[x]) if(!vis[t]) getsz(t); vis[x] = 0;}
int getmn(int x){
vis[x] = 1; int mx = 0 , sz = 1;
for(auto t : ch[x]) if(!vis[t]){int z = getmn(t); sz += z; mx = max(mx , z);}
vis[x] = 0; if(msz > (mx = max(mx , nsz - sz))){msz = mx; mid = x;} return sz;
}
void get(int x , mean c){
for(auto t : qry[x]){
int y = l[t] + r[t] - x;
if(in[y]) for(int i = 0 ; i < K ; ++i) ans[t] = (ans[t] + 1ll * c[i] * now[y][(K - i) % K]) % MOD;
}
vis[x] = 1; for(auto t : ch[x]) if(!vis[t]) get(t , add(c , val[t])); vis[x] = 0;
}
void mdy(int x , mean c){vis[x] = in[x] = 1; now[x] = c; for(auto t : ch[x]) if(!vis[t]) mdy(t , add(c , val[t])); vis[x] = 0;}
void clr(int x){vis[x] = 1; in[x] = 0; for(auto t : ch[x]) if(!vis[t]) clr(t); vis[x] = 0;}
void solve(int x){
nsz = 0; getsz(x); if(nsz == 1) return;
msz = 1e9; getmn(x); x = mid; now[x] = unit(); in[x] = vis[x] = 1;
mean tp = add(unit() , val[x]); for(auto t : ch[x]) if(!vis[t]){get(t , add(tp , val[t])); mdy(t , add(unit() , val[t]));}
in[x] = 0; for(auto t : ch[x]) if(!vis[t]) clr(t);
for(auto t : ch[x]) if(!vis[t]) solve(t);
}
int main(){
N = read(); K = read();
for(int i = 1 ; i < N ; ++i){int p = read() , q = read(); ch[p].push_back(q); ch[q].push_back(p);}
for(int i = 1 ; i <= N ; ++i) val[i] = read();
Q = read();
for(int i = 1 ; i <= Q ; ++i)
if((l[i] = read()) == (r[i] = read())) ans[i] = !val[i];
else{qry[l[i]].push_back(i); qry[r[i]].push_back(i);}
solve(1); for(int i = 1 ; i <= Q ; ++i) printf("%d\n" , ans[i]);
return 0;
}
CF765F
sol 将询问离线然后枚举右端点,维护每一个位置$i$与其之后的所有位置$j$的距离最小值然后区间取min。显然这样的做法的最劣复杂度是$O(n^2)$的,我们需要一些剪枝。
我们考虑对于某一个位置$i$,在其之后 真正有可能更新最小值的位置有哪些。如果设序列$\{a\}$是一个递增序列,$a_1 > i,val_{a_1} \geq val_i$,且每当右端点取到其中的某个$a_j$时$i$位置的最小值会进行有效的更新,那么会要满足:$\forall j \in [1,|a|) , |val_i - val_{a_j}| \geq 2|val_i - val_{a_{j+1}}|$,这是因为如果这个条件不满足那么$a_j$与$a_{j+1}$的距离会更小,此时对$i$的更新就是无效的。
不难发现上面的$a$序列的长度最多只会有$log 10^9$项,所以我们可以使用线段树找出当右端点移到哪些位置的时候需要修改$i$位置,然后移动右端点进行更新并统计答案。复杂度$O(nlog^2n+qlogn)$
我们考虑对于某一个位置$i$,在其之后 真正有可能更新最小值的位置有哪些。如果设序列$\{a\}$是一个递增序列,$a_1 > i,val_{a_1} \geq val_i$,且每当右端点取到其中的某个$a_j$时$i$位置的最小值会进行有效的更新,那么会要满足:$\forall j \in [1,|a|) , |val_i - val_{a_j}| \geq 2|val_i - val_{a_{j+1}}|$,这是因为如果这个条件不满足那么$a_j$与$a_{j+1}$的距离会更小,此时对$i$的更新就是无效的。
不难发现上面的$a$序列的长度最多只会有$log 10^9$项,所以我们可以使用线段树找出当右端点移到哪些位置的时候需要修改$i$位置,然后移动右端点进行更新并统计答案。复杂度$O(nlog^2n+qlogn)$
#include
using namespace std;
int read(){
int a = 0; char c = getchar(); bool f = 0;
while(!isdigit(c)){f = c == '-'; c = getchar();}
while(isdigit(c)){
a = a * 10 + c - 48; c = getchar();
}
return f ? -a : a;
}
const int _ = 3e5 + 7;
int cnt , N , M , arr[_] , lsh[_] , id[_] , L[_] , R[_] , ans[_]; vector < int > Q[_] , pos[_];
namespace segt{
int mn[_ << 2];
#define mid ((l + r) >> 1)
#define lch (x << 1)
#define rch (x << 1 | 1)
void clear(){memset(mn , 0x3f , sizeof(mn));}
void modify(int x , int l , int r , int tar , int val){
mn[x] = min(mn[x] , val); if(l == r) return;
mid >= tar ? modify(lch , l , mid , tar , val) : modify(rch , mid + 1 , r , tar , val);
}
int qry(int x , int l , int r , int L , int R){
if(l >= L && r <= R) return mn[x];
int mn = 1e9;
if(mid >= L) mn = qry(lch , l , mid , L , R);
if(mid < R) mn = min(mn , qry(rch , mid + 1 , r , L , R));
return mn;
}
}using namespace segt;
int main(){
N = read(); segt::clear(); for(int i = 1 ; i <= N ; ++i) arr[i] = lsh[i] = read();
M = read(); for(int i = 1 ; i <= M ; ++i){L[i] = read(); R[i] = read(); Q[R[i]].push_back(i);}
sort(lsh + 1 , lsh + N + 1); cnt = unique(lsh + 1 , lsh + N + 1) - lsh - 1;
for(int i = 1 ; i <= N ; ++i) id[i] = lower_bound(lsh + 1 , lsh + cnt + 1 , arr[i]) - lsh;
for(int i = N , cur , t ; i ; --i){
cur = cnt;
while((t = segt::qry(1 , 1 , cnt , id[i] , cur)) <= N){
pos[t].push_back(i); if(id[t] == id[i]) break;
cur = upper_bound(lsh + 1 , lsh + cnt + 1 , (arr[i] + arr[t]) / 2) - lsh - 1;
}
cur = 1;
while((t = segt::qry(1 , 1 , cnt , cur , id[i])) <= N){
pos[t].push_back(i); if(id[t] == id[i]) break;
cur = lower_bound(lsh + 1 , lsh + cnt + 1 , (arr[i] + arr[t] + 1) / 2) - lsh;
}
segt::modify(1 , 1 , cnt , id[i] , i);
}
segt::clear();
for(int i = 1 ; i <= N ; ++i){
for(auto t : pos[i]) segt::modify(1 , 1 , N , t , abs(arr[i] - arr[t]));
for(auto t : Q[i]) ans[t] = segt::qry(1 , 1 , N , L[t] , i);
}
for(int i = 1 ; i <= M ; ++i) printf("%d\n" , ans[i]);
return 0;
}
HDU5845
sol 对数组做前缀异或和然后有很显然的DP:设$f_i$表示前$i$个位置的最大分裂数,$f_i = \max\limits_{j=i-L}^{i-1} f_j[arr_j \bigoplus arr_i \leq X]$。
这个转移可以放到Trie树上做,我们只需要对于每一个节点维护其子树中位置在$[i-L,i-1]$范围内的最大DP值。最开始很呆地用了一个堆然后果然TLE了……
注意到这里的$i$是递增的,$i-L,i-1$也就都是递增的,所以就是一个滑动窗口问题,用一个单调队列就行了。这个单调队列最好用手写链表实现,用STL有很大概率被卡空间。
这个转移可以放到Trie树上做,我们只需要对于每一个节点维护其子树中位置在$[i-L,i-1]$范围内的最大DP值。最开始很呆地用了一个堆然后果然TLE了……
注意到这里的$i$是递增的,$i-L,i-1$也就都是递增的,所以就是一个滑动窗口问题,用一个单调队列就行了。这个单调队列最好用手写链表实现,用STL有很大概率被卡空间。
#include
using namespace std;
int read(){
int a = 0; char c = getchar(); bool f = 0;
while(!isdigit(c)){f = c == '-'; c = getchar();}
while(isdigit(c)){
a = a * 10 + c - 48; c = getchar();
}
return f ? -a : a;
}
const int _ = 1e5 + 7 , MOD = (1 << 28) - 1;
int dp[_] , Arr[_] , T , N , X , L;
namespace Trie{
const int __ = _ * 28;
int ch[__][2] , hd[__] , tl[__] , cnt;
struct node{int pre , suf , id;}now[__]; int cntn;
void push(int x , int id){
now[++cntn].id = id; now[cntn].suf = 0;
while(tl[x]){
int t = now[tl[x]].id;
if(dp[t] > dp[id]) break;
!now[tl[x]].pre ? hd[x] = tl[x] = 0 : tl[x] = now[tl[x]].pre;
}
if(now[cntn].pre = tl[x]) now[tl[x]].suf = cntn;
if(!hd[x]) hd[x] = cntn; tl[x] = cntn;
}
void ins(int val , int id){
int rt = 1;
for(int i = 27 ; ~i ; --i){
bool flg = val >> i & 1; if(!ch[rt][flg]) ch[rt][flg] = ++cnt;
push(rt = ch[rt][flg] , id);
}
}
int query(int id , int d){
while(hd[id] && now[hd[id]].id < d)
now[hd[id]].suf ? now[hd[id] = now[hd[id]].suf].pre = 0 : hd[id] = tl[id] = 0;
return hd[id] ? dp[now[hd[id]].id] : -1e9;
}
int qry(int val , int d){
int rt = 1 , mx = -1e9;
for(int i = 27 ; (~i) && rt ; --i){
bool flg = val >> i & 1;
if(X >> i & 1){mx = max(mx , query(ch[rt][flg] , d)); rt = ch[rt][flg ^ 1];}
else rt = ch[rt][flg];
}
return max(mx , query(rt , d));
}
void clear(){memset(ch , 0 , sizeof(ch)); memset(hd , 0 , sizeof(hd)); memset(tl , 0 , sizeof(tl)); cnt = 1; cntn = 0;}
}
int main(){
for(T = read() ; T ; --T){
N = read(); X = read(); L = read(); Trie::clear();
Arr[1] = read(); int P = read() , Q = read();
for(int i = 2 ; i <= N ; ++i) Arr[i] = (1ll * Arr[i - 1] * P + Q) & MOD;
Trie::ins(0 , 0); for(int i = 2 ; i <= N ; ++i) Arr[i] ^= Arr[i - 1];
for(int i = 1 ; i <= N ; ++i){
dp[i] = Trie::qry(Arr[i] , i - L) + 1; Trie::ins(Arr[i] , i);
//cerr << dp[i] << endl;
}
printf("%d\n" , dp[N] > 0 ? dp[N] : 0);
}
return 0;
}
0917
HDU4689
sol 暴力$O(2^nn)$是过不了的!我们考虑依次让数和位置进行匹配。设$f_{i,j}$表示考虑了前$i$个数和位置,有$j$个‘+’号位置没有匹配(等价的,即有$j$个数还没有匹配到'-'号)的方案数。
如果当前考虑的位置$i$上的字符是+,那么一定要等着之后的一个位置向这个位置丢一个数过来匹配;如果当前位置是-,则一定要从之前的没有匹配的数中选择一个进行匹配。同时$i$数字还有两种选择:向前匹配一个+或者向后匹配一个-。乘上选择匹配的+号和-号的方案数就可以得到转移方程。
如果当前考虑的位置$i$上的字符是+,那么一定要等着之后的一个位置向这个位置丢一个数过来匹配;如果当前位置是-,则一定要从之前的没有匹配的数中选择一个进行匹配。同时$i$数字还有两种选择:向前匹配一个+或者向后匹配一个-。乘上选择匹配的+号和-号的方案数就可以得到转移方程。
#include
using namespace std;
long long dp[23][23]; char str[23];
int main(){
while(scanf("%s" , str + 1) != EOF){
int L = strlen(str + 1);
memset(dp , 0 , sizeof(dp)); dp[0][0] = 1;
for(int i = 0 ; i < L ; ++i)
if(str[i + 1] == '+')
for(int j = 0 ; j <= i ; ++j){dp[i + 1][j + 1] += dp[i][j]; dp[i + 1][j] += dp[i][j] * j;}
else for(int j = 1 ; j <= i ; ++j){dp[i + 1][j] += dp[i][j] * j; dp[i + 1][j - 1] += dp[i][j] * j * j;}
printf("%lld\n" , dp[L][0]);
}
return 0;
}
小Y和二叉树
sol 先弄出能够位于中序遍历第一个位置的点。它显然是树的左儿子链的链底。任何一个度数$\leq 2$的点都可以作为这样的点,所以在这样的点中找到编号最小的点记为$x$。接下来我们的思路是自下而上构建出对应树的左儿子链并构造每个左儿子链上的点的右子树。
以$x$为根对原树做一遍树形DP:设$f_i$表示以$i$为根的子树内可以作为这棵子树的中序遍历第一个元素的最小编号。注意如果$i$只有一个孩子那么可以将其作为$i$的右儿子从而使$i$作为第一个位置。
接下来设$solve(l,f)$表示正在构造点$l$的子树、$l$是否在左儿子链上。分类讨论:
当$f=1$且原树上$l$只有一个孩子$p$时,$p$可以作为$l$的父亲或者$l$的右儿子,两者下一个位置的中序遍历最小值为$p$和$f_p$,故如果$p \leq f_p$,那么让$p$作为$l$的父亲更优,进入$solve(p,1)$,否则进入$solve(p,0)$;
当$f=1$且原树上$l$有两个孩子$p,q$时,一定是一个作为$l$的右儿子,一个作为$l$的父亲。因为即将需要遍历的是右子树,所以如果$f_p < f_q$,则让$p$作为右儿子更优,即进入$solve(p,0),solve(q,1)$,反之执行$solve(q,0),solve(p,1)$。
当$f=0$且原树上$l$只有一个孩子$p$时,如果$f_l=l$则让$p$作为右子树,否则作为左子树,并进入$solve(p,0)$
当$f=0$且原树上$l$有两个孩子$p,q$时,如果$f_p < f_q$则先递归$p$再递归$q$,否则先递归$q$再递归$p$。
思路很清晰,实现起来也很简单。
以$x$为根对原树做一遍树形DP:设$f_i$表示以$i$为根的子树内可以作为这棵子树的中序遍历第一个元素的最小编号。注意如果$i$只有一个孩子那么可以将其作为$i$的右儿子从而使$i$作为第一个位置。
接下来设$solve(l,f)$表示正在构造点$l$的子树、$l$是否在左儿子链上。分类讨论:
当$f=1$且原树上$l$只有一个孩子$p$时,$p$可以作为$l$的父亲或者$l$的右儿子,两者下一个位置的中序遍历最小值为$p$和$f_p$,故如果$p \leq f_p$,那么让$p$作为$l$的父亲更优,进入$solve(p,1)$,否则进入$solve(p,0)$;
当$f=1$且原树上$l$有两个孩子$p,q$时,一定是一个作为$l$的右儿子,一个作为$l$的父亲。因为即将需要遍历的是右子树,所以如果$f_p < f_q$,则让$p$作为右儿子更优,即进入$solve(p,0),solve(q,1)$,反之执行$solve(q,0),solve(p,1)$。
当$f=0$且原树上$l$只有一个孩子$p$时,如果$f_l=l$则让$p$作为右子树,否则作为左子树,并进入$solve(p,0)$
当$f=0$且原树上$l$有两个孩子$p,q$时,如果$f_p < f_q$则先递归$p$再递归$q$,否则先递归$q$再递归$p$。
思路很清晰,实现起来也很简单。
#include
using namespace std;
#define pb push_back
int rd(){int a; scanf("%d" , &a); return a;}
const int _ = 1e6 + 7; int N , mn[_]; vector < int > ch[_] , ans;
int dfs(int x , int p){
mn[x] = ch[x].size() == 1 ? x : 1e9; int cnt = 0;
for(int t : ch[x]) if(t - p){mn[x] = min(mn[x] , dfs(t , x)); ++cnt;}
return cnt - 1 ? mn[x] : (mn[x] = min(mn[x] , x));
}
bool vis[_];
void gt(int x , bool flg = 0){
vis[x] = 1; int p = 0 , q = 0;
if(flg){
ans.pb(x); for(int t : ch[x]) if(!vis[t]) !p ? p = t : q = t;
if(q){gt(mn[p] < mn[q] ? p : q); gt(mn[p] > mn[q] ? p : q , 1);}
else if(p) gt(p , mn[p] >= p);
return;
}
if(ch[x].size() == 1) return ans.pb(x);
if(ch[x].size() == 2){if(mn[x] == x) ans.pb(x); for(int t : ch[x]) if(!vis[t]) gt(t); if(mn[x] - x) ans.pb(x);}
else{
for(int t : ch[x]) if(!vis[t]) p ? q = t : p = t;
gt(mn[p] < mn[q] ? p : q); ans.pb(x); gt(mn[p] > mn[q] ? p : q);
}
}
int main(){
N = rd(); for(int i = 1 ; i <= N ; ++i) for(int j = rd() ; j ; --j) ch[i].pb(rd());
for(int i = 1 ; i <= N ; ++i) if(ch[i].size() <= 2){dfs(i , 0); gt(i , 1); break;}
for(int t : ans) printf("%d " , t);
}
0918
UVA11605
sol 我们显然要分别考虑每一个块的贡献,不难得到一个块$(i,j,k)$在$k$次操作后对答案的贡献是$g(x) = (Px+(1-P))^k$的所有奇数次项的和,其中$P = \frac{f(N,i)f(M,j)f(P,k)}{N^2M^2K^2},f(i,j) = i^2-(j-1)^2-(i-j)^2$。
我们要求出$g(x)$的所有奇数次项的和,直接用$\frac{g(1) - g(-1)}{2}$求就行了。复杂度$O(TNMPlogk)$也不知道为什么能过
我们要求出$g(x)$的所有奇数次项的和,直接用$\frac{g(1) - g(-1)}{2}$求就行了。复杂度$O(TNMPlogk)$也不知道为什么能过
#include
using namespace std;
int main(){
int T , N , M , P , K , Case = 0;
for(cin >> T ; T ; --T){
cin >> N >> M >> P >> K; double ans = 0;
for(int i = 1 ; i <= N ; ++i)
for(int j = 1 ; j <= M ; ++j)
for(int k = 1 ; k <= P ; ++k){
double p = 1 - ((i - 1.0) * (i - 1) + (N - i) * (N - i)) / N / N ,
q = 1 - ((j - 1.0) * (j - 1) + (M - j) * (M - j)) / M / M,
r = 1 - ((k - 1.0) * (k - 1) + (P - k) * (P - k)) / P / P;
ans += (1 - pow(1 - 2 * p * q * r , K)) / 2;
}
printf("Case %d: %.8lf\n" , ++Case , ans);
}
return 0;
}
HDU4089
sol 考虑DP:设$f_{i,j}$表示期望到达有$i$个人、Tomato在第$j$个位置的状态的期望次数,那么答案就是$\sum\limits_{i=1}^n p3f_{i,1} + p4\sum\limits_{j=k}^n f_{i,j}$
转移:$f_{i,j} = p1f_{i,j} + p2f_{i,(j \mod n) +1} + p3f_{i+1,j+1}+[i==n][j==m]$。
从大到小考虑每个$i$,相当于对当前所有$f_{i,j}$求解$i$元线性方程组。因为方程未知数总数只有$2i$所以稍加处理即可将复杂度变为$O(n^2)$。
注意开滚动数组。
转移:$f_{i,j} = p1f_{i,j} + p2f_{i,(j \mod n) +1} + p3f_{i+1,j+1}+[i==n][j==m]$。
从大到小考虑每个$i$,相当于对当前所有$f_{i,j}$求解$i$元线性方程组。因为方程未知数总数只有$2i$所以稍加处理即可将复杂度变为$O(n^2)$。
注意开滚动数组。
#include
using namespace std;
int N , M , K; double dp[2][2003] , func[2003][3] , p1 , p2 , p3 , p4;
void solve(int len){
double p = func[len][0] , q = func[len][1] , r = func[len][2];
for(int i = 1 ; i < len ; ++i){
double xs = q / func[i][0];
q = -xs * func[i][1]; r -= func[i][2] * xs;
}
func[len][2] = r / (p + q);
for(int i = len - 1 ; i ; --i) func[i][2] = (func[i][2] - func[i + 1][2] * func[i][1]) / func[i][0];
}
int main(){
while(cin >> N >> M >> K >> p1 >> p2 >> p3 >> p4){
if(p4 < 1e-6){puts("0.00000"); continue;}
double ans = 0; int now = 0; memset(dp , 0 , sizeof(dp));
for(int i = N ; i ; --i){
now ^= 1;
for(int j = 1 ; j <= i ; ++j){
func[j][0] = 1 - p1; func[j][1] = -p2;
func[j][2] = p3 * dp[now ^ 1][j + 1] + (i == N && j == M);
}
solve(i); for(int j = 1 ; j <= i ; ++j) dp[now][j] = func[j][2];
ans += dp[now][1] * p3;
for(int j = K + 1 ; j <= i ; ++j) ans += dp[now][j] * p4;
}
printf("%.5lf\n" , 1 - ans);
}
return 0;
}
CF1188C
sol 考虑计算对于所有$a$,权值$\geq a$的序列数量,求和即为答案。
将原序列排序,那么相邻之间的差的最小值就是整个序列的最小值。考虑DP:设$f_{i,j}$表示考虑到第$i$个点、序列中选择了$j$个点的方案数。转移使用单调指针+前缀和优化。
做到这里以为复杂度是$O(10^5nk)$的,结果……序列极差极小值的最大值是$\frac{10^5}{k-1}$,所以只要做到这么多就可以了。
复杂度$O(10^5n)$,我可真是个×××。
将原序列排序,那么相邻之间的差的最小值就是整个序列的最小值。考虑DP:设$f_{i,j}$表示考虑到第$i$个点、序列中选择了$j$个点的方案数。转移使用单调指针+前缀和优化。
做到这里以为复杂度是$O(10^5nk)$的,结果……序列极差极小值的最大值是$\frac{10^5}{k-1}$,所以只要做到这么多就可以了。
复杂度$O(10^5n)$,我可真是个×××。
#include
using namespace std;
const int _ = 1003 , MOD = 998244353;
int N , K , arr[_] , num[_][_] , ans;
int add(int a , int b){return a + b >= MOD ? a + b - MOD : a + b;}
int main(){
cin >> N >> K; for(int i = 1 ; i <= N ; ++i) cin >> arr[i];
sort(arr + 1 , arr + N + 1);
for(int i = 1 ; i <= 1e5 / (K - 1) ; ++i){
int pos = 0;
for(int j = 1 ; j <= N ; ++j){
memset(num[j] , 0 , sizeof(int) * (K + 1)); num[j][1] = j;
while(arr[j] - arr[pos + 1] >= i) ++pos;
for(int k = 2 ; k <= K ; ++k) num[j][k] = add(num[pos][k - 1] , num[j - 1][k]);
}
ans = add(ans , num[N][K]);
}
cout << ans; return 0;
}
CF Round584
晚自习VP了一下练练手感,Rank73感觉如果打了可以上2500+。
E2看完题解发现自己是个沙雕……
写完E1看F发现\(O(mlog^2n)\)做法太过显然还跑得飞快……
G1最开始看错题以为每次可以只交换一个……
部分题解