A題:
A題題目描述:
Home W的數學
我們都知道,Home W的數學最厲害了。有一天,他又開始開動腦筋了,他想:“爲什麼數字總是要從1排列到n呢?”於是,Home W開始研究自己排列數字的方法。首先,他寫下了1-n中所有的奇數(按照升序排列),然後他又寫下了1-n中所有的偶數(按照升序排列),那麼問題來了,在這樣的排列方式下第k個數是什麼呢?
輸入只有一行,包括n和k(1 ≤ k ≤ n ≤ 1012).
注意:長整型聲明形式爲: long long a;
在這裏的輸入形式爲:scanf("%I64d",&a); (或者使用cin也可)
輸出形式爲:printf("%I64d\n",a); (或者使用cout也可)
輸出只有一行,輸出第k個數即可。
10 3
5樣例說明(非輸出部分): 按照Home W的排列方式即爲{1,3,5,7,9,2,4,6,8,10},那麼顯然第三個數是5
7 7
題意很明顯,我們可以分奇偶的情況進行考慮:
當n爲偶數的時候,顯然前n/2個數爲奇數,後n/2個數爲偶數,假設第1-n個數的下標分別是1~n,則前n/2個數分別爲2*k-1,而後n/2
個數2*(k - n/2)。
當n爲奇數的時候,則前(n+1)/2個數爲奇數,後(n+1)/2-1個數爲偶數(相當於在最後補上缺少的那個偶數,實際上不存在),同樣的前
(n+1)/2個數分別爲2*k-1,後(n+1)/2-1個數爲2*(k-n/2)。
完整代碼實現:
#include <cstdio>
typedef long long LL;
void solve(LL sum, LL index){
LL mid = (sum + 1) / 2;
if(index <= mid){
printf("%I64d\n",index*2-1);
}else{
printf("%I64d\n",(index - mid)*2);
}
}
int main(){
LL n,k;
while(scanf("%I64d %I64d",&n,&k)!=EOF){
solve(n,k);
}
return 0;
}
B題:
題目描述:
QAQ和香蕉
QAQ是個吃貨,這一次他來到了一個商店,想買w根香蕉,但是這個商店實在是黑,他需要支付k元買第一根香蕉,2k元買第二根香蕉....(也就是說,當他買第k根香蕉時,他需要支付i*k元)。
可是QAQ錢包裏只有n元,你能幫助他計算一下,他要借多少錢才能買下w根香蕉嗎?
第一行包括三個整數 k, n, w (1 ≤ k, w ≤ 1000, 0 ≤ n ≤ 109), 分別是第一根香蕉的單價,QAQ錢包裏的錢總數,以及他想要買的香蕉總數。
輸出只有一行,包含一個整數——QAQ需要借多少錢,如果他不需要借錢,輸出0。
3 17 4
13
我們可以先計算w根香蕉的總價格爲:k+2*k+...+w*k = (1+w) * w / 2 * k,因此如果QAQ錢包裏的錢大於總價格,顯然輸出0,否
則的話則輸出其差值。
完整代碼實現:
#include <cstdio>
void solve(int unitPrice,int allMoney,int bananaAmount){
int borrowMoney = bananaAmount * (bananaAmount + 1) / 2 * unitPrice - allMoney;
if(borrowMoney <= 0){
printf("0\n");
}else{
printf("%d\n",borrowMoney);
}
}
int main(){
int k,n,w;
while(scanf("%d %d %d",&k,&n,&w)!=EOF){
solve(k,n,w);
}
return 0;
}
C題
題目描述:
QAQ的數學題
Home W說他數學很好,QAQ表示不服氣,於是QAQ出了一道數學題給Home W做。題目很簡短:給定n個數字,每個數字最多選擇一次(也可以不選),問這n個數字不能組合得到的最小的值,並輸出。
輸入到文件結束( 即輸入格式爲 while(scanf(...)!=EOF)){ ... } )
第一行包含一個整數N(1 <= N <= 1000),第二行爲N個整數Pi(0 <= Pi <=
10000).
輸出只有一個整數,表示這n個數字不能組合得到的最小的值。
4 1 2 3 4
11
這道題的思路比較巧妙,順着題目的角度去思考很難入手,因爲這樣的話要一個個的遍歷過去,然後再計算已經遍歷過的序列
能夠表示哪些數,而且不好判斷,因爲可以從數組中任意選擇數字,順着題目的角度很難入手,那爲什麼我們不考慮相反的方向呢?
考慮數組a不能表示哪些數,那我們假如現在遍歷到a[i]元素時,此時數組能組合成1~sum中的任意數字,那麼此時考慮a[i+1],
那怎麼才能知道數組a不能表示哪些數呢?很顯然,由於此時數組已經能組合成1~sum中的任意數字,那麼當a[i+1] > sum + 1時,則
此時sum+1相當於被跳過了。顯然sum+1是不能被數組元素組合成的。而當a[i+1] <= sum + 1時,顯然此時可以組合成的數字的範
圍擴大爲1 ~ sum + a[i+1]。所以抓住了sum+1這個臨界條件,問題就解決了。
所以我們可以先排序,然後按照上述過程處理,排序的目的是爲了找到最小的不能組合成的數字。
完整代碼實現:
#include <cstdio>
#include <algorithm>
using namespace std;
const int MAX_SIZE = (int) 1e3 + 10;
int a[MAX_SIZE];
void solve(int sum){
for(int i = 0;i < sum;i++)
scanf("%d",&a[i]);
sort(a,a + sum);
int ans = 0;
for(int i = 0;i < sum;i++){
if(a[i] > ans + 1){
break;
}else{
ans += a[i];
}
}
printf("%d\n",ans + 1);
}
int main(){
int n;
while(scanf("%d",&n)!=EOF){
solve(n);
}
return 0;
}
D題:
題目描述:
running jump的樹
我們都知道,running jump數據結構最厲害了,於是這一天,他又創造了一種新的數據結構,叫做k-樹.那什麼是k-樹呢?首先,k-樹時一個無限節點的樹,意
思是說這棵樹是可以不斷往下延伸的,並且k-樹有以下的性質:
(1).每個節點有k個子節點
(2).每條邊都有一個權重,每條邊的權重從左往右一次爲1,2,3,...,k.
(感覺好神奇的樣子
下圖是3-樹的一部分(因爲節點是無限的,所以還可以往下無限延伸)
這時候,我們的running jump開始給我們出題了,他說:“從根節點開始,有多少條路徑的權值之和爲n呢?”然後他又想了想,感覺題目太容易了,於是又
加了一個限制條件,路徑中至少要有一條邊的權重大於等於d。那麼聰明的Acmer,你能解決running jump給我們留下的這個問題嗎?
由於結果可能過大,因此將結果對1000000007(109 + 7)取餘後輸出.
輸入只有一行,包括三個整數,n, k and d (1 ≤ n, k ≤ 100; 1 ≤ d ≤ k).
輸出只有一行,包含對1000000007 (109 + 7)取餘後的結果.
3 3 2
3
3 3 3
1
4 3 2
6
4 5 2
7
由於題目給了限制條件,所以給我們的思考帶來了一定的障礙,那麼我們可以先將問題化簡,如果沒有至少含有一條權重大於
等於d的路徑這個條件的話,該如何考慮呢?
如果沒有了這個限制條件,那麼就相當於從 1 ~ k 中選擇一些數字,使得他們的和爲n,每個數字可以選擇多次,很直接的思路就是搜索,但是
搜索的時間複雜度是指數級的,而n,k範圍比較大(n,k <= 100),因此搜索是不可取的。那考慮權值之和爲j時,剩下需要解決的則是權值之和爲n-j
的子問題。而n-j的子問題依舊可以按照以上方式繼續分解,很顯然,利用這種方式考慮子問題推出更大問題的解時,原問題不需要考慮子問題是如何
達到當前狀態的。因此符合無後效性,並且原問題下的子問題的解也是當前找到的最優解(即統計好了所有路徑和爲j的路徑條數),因此該問題符合最優
子結構性質。可用動態規劃求解。
既然可用動態規劃求解,我們利用剛剛找到的無後效性的狀態,原問題拆分爲j和n-j規模的子問題,而k-樹共有k個分支。很顯然我們可以推出狀態
轉移方程即是:
dp[n] = dp[n-1] + dp[n-2] + ... + dp[n-k];
再考慮限制條件:至少含有一條權值大於等於d的邊,很顯然,對於這種至少含有一個,一種,一條的一類的問題,正難則反,我們則考慮所有
的邊權值都小於d,因此我們只要將權值和爲n,分支數爲k的總的路徑數減去權值和爲n,分支數爲b-1(這樣的話最大權值的邊也就是b-1)的路徑數
即得我們所求的答案。因此只要將狀態轉移方程增加一維表示分支數不同的狀態值求解即可。
在確定狀態轉移方程遞推原問題的過程中,確定一些狀態的初始值,很顯然,dp[0]即爲根節點處,權值爲0的只有在這一點,因此兩種分支數的
情況下dp[0] 均等於1,最後,小心取模即可。
完整代碼實現:
#include <cstdio>
#include <cstring>
const int MAX_SIZE = 105;
const int MOD = 1e9+7;
int dp[2][MAX_SIZE];
void solve(int n,int k,int d){
memset(dp,0,sizeof(dp));
dp[0][0] = dp[1][0] = 1;
for(int i = 1;i <= n;i++){
for(int j = 1;j <= k;j++){
if(i - j < 0){
break;
}else{
dp[0][i] += dp[0][i-j];
if(dp[0][i] >= MOD){
dp[0][i] -= MOD;
}
}
}
for(int j = 1;j < d;j++){
if(i - j < 0){
break;
}else{
dp[1][i] += dp[1][i-j];
if(dp[1][i] >= MOD){
dp[1][i] -= MOD;
}
}
}
}
printf("%d\n",((dp[0][n] - dp[1][n]) + MOD) % MOD);
}
int main(){
int n,k,d;
// freopen("cf 431C.txt","r",stdin);
while(scanf("%d %d %d",&n,&k,&d)!=EOF){
solve(n,k,d);
}
}
E題:
Value Dragon出難題了
有一天,Value Dragon覺得好無聊啊,所以決定出一道題目給自己做,於是他寫下了一個含有n個元素的整型數組a,這n個元素分別是a1,a2,...,an。
然後呢,他就想啊,如果能找到一個連續的區間[l,r](1 ≤ l ≤ r ≤ n),使得該區間中所有的數的異或值大於等於k,那他就覺得這段區間是一個完美的區間。
那麼問題來了,這樣的區間總共有多少個呢?於是Value Dragon陷入了無盡的思考中......
第一行輸入爲兩個數,分別是n和k (1 ≤ n ≤ 106, 1 ≤ k ≤ 109) — 分別表示整型數組元素的個數以及參數k的值
第二行輸入爲n個整數 ai (0 ≤ ai ≤ 109) — 數組a的n個元素
輸出只有一行,表示在數組a中,這樣的完美區間有多少個。
3 1
1 2 3
5
3 2
1 2 3
3
3 3
1 2 3
2
解析:
這道題考察的是異或,那麼對於異或,有一些重要的性質:
1.0 ^ 1 = 1,0 ^ 0 = 0,以上說明,0異或任何數的結果均爲0.
2.n^n = 0,這說明任何數與其自身異或後的結果必然爲0.
利用以上兩個性質,顯然我們可以將異或前綴和處理一下,這樣的話任意兩個前綴和進行異或後的結果以及其所有預處理好
的前綴和自身,就可以將所有連續區間遍歷完畢。這樣直接處理的話時間複雜度是O(n^2),而n <= 10 ^ 6,這樣做顯然是超時
的。那麼怎麼樣降低時間複雜度呢?
試想,對於任意一個前綴和區間,我們要做的是,找到其他滿足條件的前綴和區間或者0,使得兩者異或後的結果大於等於k,
即對於任意的前綴異或和sum,我們要做的是找到 sum ^ [] >= k,因此這是一個匹配的過程,而對於兩個數字的比較,除了直接判
斷之外,我們還可以通過比較其二進制的形式來比較其大小。比如說:數字7和數字6,四位二進制形式分別爲(0111) 和 (0110),用
這樣的方式我們當比較到兩個數的最後一位時,才能確定 0111 > 0110,而對於其二進制的存儲及比較,我們可以看成對01的存儲,
然後再匹配。
那麼這個過程我們就可以用字典樹實現,初始時字典樹只含有根節點,然後利用性質1,插入數據0。而後再一位一位的匹配比
較即可。注意k值的範圍,從而確定樹的深度。具體詳細解釋可看代碼註釋:
#include <cstdio>
const int maxMoveStep = 29; //由於kmax = 1e9,而2^30=1073741824,因此最多移位次數爲29
typedef long long ll;
struct node{
ll weight; //表示節點權值,也就是說該節點下有多少個滿足條件的數字
node *next[2]; //分別表示01字典樹的兩個數位,0 1
node(){
weight = 0;
next[0] = next[1] = NULL;
}
};
void trieInsert(node *root,int value){
node *p = root;
for(int i = maxMoveStep;i >= 0;i--){
int bit = value >> i & 1; //從最高位存至最低位
if(!p -> next[bit]){ //如果對應數位的節點不存在,則新建該節點
p -> next[bit] = new node();
}
p -> weight++;
p = p -> next[bit]; //存在則直接往下繼續遍歷即可
}
p -> weight++;
}
int trieQuery(node *root,int prefixSum,int k){
node *p = root;
ll ans = 0;
for(int i = maxMoveStep;i >= 0;i--){
int pBit = prefixSum >> i & 1;
int kBit = k >> i & 1;
if(kBit){ //如果k的該位爲1,要使最後結果大於等於k,那麼p指針就只能往pBit^1的方向移動
pBit ^= 1;
}else{ //如果k的該位爲0,顯然pBit^1方向的子樹均滿足條件,則可以將其結果計算進來
//然後p指針再往pBit方向遍歷,考慮剩下半邊子樹滿足條件的部分
if(p -> next[pBit^1]){
ans += p -> next[pBit^1] -> weight;
}
}
p = p -> next[pBit];
if(!p){
return ans;
}
}
return ans + p -> weight; //考慮到了根節點,因此還要加上異或後結果等於k的部分
}
void solve(int n,int k){
int number,sum = 0;
ll cnt = 0;
node *root = new node(); //01字典樹的根節點
trieInsert(root,0); //插入初始值,以方便考慮到所有的情況,包括前綴異或和其本身
for(int i = 0;i < n;i++){
scanf("%d",&number);
sum ^= number;
cnt += trieQuery(root,sum,k);
trieInsert(root,sum);
}
printf("%I64d\n",cnt);
delete root;
}
int main(){
int n,k;
// freopen("cf 665E.txt","r",stdin);
while(scanf("%d %d",&n,&k)!=EOF){
solve(n,k);
}
return 0;
}
總結:這次題目的總體難度不大,考察的知識點不多,但是01字典樹還是很常用的,很多異或的問題都是用01字典樹解決,第四
題的dp題目屬於比較簡單的dp題,需要找相似的子問題,然後狀態轉移方程也不難推導出來。