
1. 從“數單詞”到理解字符串處理的本質最近在輔導一些剛接觸編程的同學時發現一個挺有意思的現象很多人拿到“計算一行單詞長度”這樣的題目第一反應是去翻書找有沒有現成的“單詞分割函數”。這本身沒錯但如果你只停留在調用split()然后循環輸出len()的層面那可能就錯過了這道題背后更重要的東西——對輸入流和字符串底層邏輯的親手把控。這道來自《信息學奧賽一本通》的1142題表面看是簡單的輸入輸出和字符串處理實則是訓練我們“親手拆解數據”能力的絕佳起點。它強迫你離開高級函數的舒適區去直面最原始的字符序列理解空格如何作為分隔符以及程序如何“一個字符一個字符”地構建出對數據的認知。今天我們就來徹底拆解這道題不僅給出能AC的代碼更要弄明白每一步“為什么這么做”以及在這個過程中那些容易栽跟頭的細節。2. 題目深度解析邊界條件與核心陷阱題目描述很短“輸入一行單詞序列相鄰單詞之間由1個或多個空格間隔請對應地計算各個單詞的長度。” 但這里面藏著好幾個需要明確的關鍵點也是評判程序是否健壯的核心。2.1 輸入格式的明確與“一行”的含義首先“輸入一行”這個描述在編程中需要精確化。在控制臺或文件輸入中“一行”通常意味著以換行符\n作為結束標志的一段字符序列。對于C可能是cin配合getline對于Python就是input()。這里的關鍵是我們必須一次性讀入整行而不是用cin stringC或input().split()Python的默認行為因為后者會自動忽略開頭的空白符并以下一個空白符空格、制表符、換行作為截斷點這恰恰會讓我們丟失“連續多個空格”這一關鍵信息。所以第一步必須是讀取整行。2.2 “單詞”的定義與分隔符的處理題目說“相鄰單詞之間由1個或多個空格間隔”。這意味著分隔符唯一只有空格 是分隔符不包括制表符\t或其他空白字符。這是一個簡化但在嚴格的在線評測OJ環境下我們必須嚴格按照題意來。空格數量不定可能是1個也可能是多個。這直接否決了用“遇到一個空格就切分”的簡單邏輯因為多個連續空格會產生空字符串片段這些片段不是單詞必須被跳過。開頭和結尾可能有空格輸入的行開頭和結尾也可能有空格。這些空格不構成單詞在計算時必須被忽略。這是最常見的陷阱之一很多人的程序在遇到 hello world 這樣的輸入時就會出錯。2.3 輸出格式的精確對應“對應地計算各個單詞的長度”意味著輸出順序必須與單詞在輸入行中出現的順序嚴格一致。輸出通常是以空格分隔的每個單詞的長度。如果一行沒有單詞例如輸入全是空格根據常規邏輯應該沒有輸出或者輸出一個空行。這些細節需要在編碼前就想清楚。3. 核心算法設計狀態機與雙指針法解決這類問題有兩種主流的底層思想狀態機State Machine和雙指針Two Pointers。它們都不依賴于高級的字符串分割函數能讓我們更透徹地理解過程。3.1 狀態機思路清晰地刻畫程序“思維”我們可以把程序讀取字符的過程看作是在幾個狀態間切換狀態0尋找單詞開始初始狀態。程序逐個讀取字符如果遇到非空格字符說明找到了一個單詞的開頭記錄當前位置并切換到狀態1。狀態1記錄單詞中。繼續讀取字符只要不是空格就認為字符屬于當前單詞持續累加長度或記錄位置。一旦遇到空格說明當前單詞結束輸出其長度然后切換回狀態0。這個思路邏輯非常清晰尤其適合用while循環和if-else來實現。它明確區分了“在單詞外”和“在單詞內”兩種情形不容易出錯。C狀態機實現示例#include iostream #include string using namespace std; int main() { string line; getline(cin, line); // 讀取整行 int n line.length(); int i 0; bool inWord false; // 狀態標志是否處于單詞內 int wordLen 0; while (i n) { if (line[i] ! ) { // 當前字符不是空格 if (!inWord) { // 如果之前不在單詞內說明是單詞開頭 inWord true; wordLen 1; // 開始計數 } else { // 已經在單詞內繼續計數 wordLen; } } else { // 當前字符是空格 if (inWord) { // 如果之前是在單詞內說明單詞結束了 cout wordLen ; inWord false; wordLen 0; } // 如果之前就不在單詞內說明是連續空格直接跳過 } i; } // 循環結束后檢查是否最后一個字符是單詞結尾即行末沒有空格結尾 if (inWord) { cout wordLen; } return 0; }3.2 雙指針思路高效定位單詞邊界雙指針法更直觀一些。我們用兩個“指針”通常是整數索引i和j來在字符串上滑動。指針i負責尋找單詞的起始位置。它不斷向前移動跳過所有的空格直到指向一個非空格字符。此時i的位置就是單詞的開始。指針j從i開始尋找單詞的結束位置。它繼續向前移動只要指向的不是空格就繼續移動。當j指向空格或字符串末尾時j-1的位置就是單詞的結束。計算單詞長度j - i。輸出長度后將i移動到j的位置即空格處然后重復步驟1開始尋找下一個單詞。這種方法代碼緊湊效率高是競賽中的常用技巧。Python雙指針實現示例line input().rstrip(\n) # 讀取整行并去掉末尾可能的換行符input()通常已處理但更安全 n len(line) i 0 first_output True # 用于控制輸出空格使格式更美觀 while i n: # 階段1跳過前導空格和單詞間的多個空格 while i n and line[i] : i 1 if i n: # 如果跳完空格已經到字符串末尾說明沒有單詞了 break # 此時 line[i] 是非空格字符即單詞開始 start i # 階段2找到這個單詞的結尾 while i n and line[i] ! : i 1 # 此時 i 指向了單詞后的第一個空格或字符串末尾 word_len i - start if not first_output: print( , end) print(word_len, end) first_output False # 循環結束所有單詞長度已輸出。如果一行沒有單詞則無輸出。 print() # 最后輸出一個換行符合多數OJ的格式要求注意上面Python示例中使用了first_output標志來控制輸出格式避免了末尾多一個空格。有些OJ系統對末尾空格不敏感但養成輸出整潔的習慣總是好的。更簡單的做法是用列表先存儲結果最后用print(*length_list)一次性輸出。4. 不同語言下的實現策略與避坑指南雖然算法思想通用但在不同編程語言中實現細節和可利用的工具庫不同也會產生不同的“坑”。4.1 C/C 實現注重手動控制與效率對于C/C選手這道題是練習字符數組C風格字符串和string類操作的經典題。坑點1輸入整行C風格用fgets(char_array, sizeof(char_array), stdin)。注意它會讀入換行符\n處理時需要判斷。C風格用getline(cin, str)。這是最推薦的方式簡單安全。坑點2遍歷與邊界手動遍歷時務必注意數組下標不要越界。在雙指針法中內層while循環的條件i n至關重要。坑點3輸出格式C中連續用cout len ;輸出最后會多一個空格。雖然很多OJ接受但嚴格的題目可能判錯。可以采用類似Python中的“首次輸出”技巧。一個健壯的C雙指針實現#include iostream #include string using namespace std; int main() { string s; getline(cin, s); int n s.size(); int i 0; bool isFirst true; // 是否是第一個輸出的數字 while (i n) { // 跳過空格 while (i n s[i] ) i; if (i n) break; // 跳過空格后到末尾結束 // 找到單詞結束位置 int j i; while (j n s[j] ! ) j; // 計算并輸出長度 if (!isFirst) cout ; cout (j - i); isFirst false; i j; // i跳到當前單詞結束的位置即空格處外層循環的i會使其進入下一個循環 } cout endl; // 輸出換行 return 0; }4.2 Python 實現簡潔背后的陷阱Python讓這道題變得極其簡單但正因為簡單更容易忽略細節。“一行代碼”的誘惑與問題print(*[len(w) for w in input().split()])這行代碼利用了input()讀取一行split()默認以任意空白字符空格、換行、制表符等分割并自動過濾掉空字符串列表推導式計算長度最后用*解包打印。對于本題它完全正確且優雅。因為題目明確分隔符是空格split()的行為完全符合要求。但是這里存在一個教學上的“陷阱”如果你只記住了這行代碼而沒有理解split()在沒有參數和有參數時的區別下次遇到“以逗號分隔”或者“嚴格以單個空格分隔需保留空單詞”的題目時就會出錯。s.split(): 按任意空白字符分割并自動移除結果中的空字符串。s.split( ): 嚴格按單個空格字符分割。如果存在連續空格會產生空字符串。所以更嚴謹的教學代碼應該這樣寫以明確意圖line input() # 明確使用空格作為分隔符但這樣會得到空字符串片段 parts line.split( ) # 因此需要過濾掉空字符串 words [part for part in parts if part ! ] # 再計算長度 lengths [len(word) for word in words] print(*lengths)雖然最終結果和split()一樣但這個過程清晰地展示了“分割-過濾-計算”的步驟加深了對字符串處理的理解。4.3 其他語言如Java的注意點Java中常用的Scanner.nextLine()讀取整行然后用line.split( )可以按一個或多個空格進行正則分割但同樣要注意開頭結尾的空格會導致空字符串。更穩健的做法是使用line.trim().split(\\s)先去掉首尾空格再按空白字符分割。但trim()可能會誤傷首尾的非空格字符本題不會所以最根本的還是手動遍歷或使用StringTokenizer類雖然已過時但思路清晰。5. 測試用例設計與調試技巧寫出代碼不算完能否通過所有邊界情況的測試才是關鍵。自己設計測試用例是一個優秀程序員必備的習慣。針對本題必須設計以下幾類測試用例普通情況hello world-5 5多個連續空格hello world-5 5開頭有空格 hello world-5 5結尾有空格hello world -5 5首尾都有空格 hello world -5 5單個單詞programming-11單個單詞帶空格 programming -11空行或純空格或 - 無輸出或輸出一個空行長單詞與短單詞混合a bc def ghij-1 2 3 4調試技巧打印中間變量在狀態機或雙指針算法中在關鍵步驟后打印i,j,inWord,wordLen等變量的值觀察其變化是否符合預期。可視化遍歷在紙上畫出字符串手動模擬你的算法用筆移動i和j指針這是理解算法最有效的方式。使用在線調試器如果環境允許使用IDE的調試功能單步執行觀察變量和程序流程。6. 從本題延伸的常見變體與解題思路掌握了本題的核心可以輕松解決一系列變體問題這也是刷題舉一反三的關鍵。變體1統計單詞個數而非長度。這更簡單只需要在發現一個單詞開始時狀態機中!inWord變為true或雙指針中找到start時計數器加1即可。變體2以特定單個字符如逗號分隔。這時分隔符不是空格了。算法完全一樣只需把判斷條件從line[i] ! 改為line[i] ! ,。但要注意如果用split(,)連續逗號會產生空字符串需要根據題目要求決定是否保留。變體3分隔符是多種字符如空格、逗號、句號。此時判斷“是否分隔符”的條件變成一個集合檢查。例如if (delimiters.find(line[i]) string::npos)C或者if line[i] not in ,.Python。核心算法框架不變。變體4不僅輸出長度還要輸出單詞本身。在記錄長度的同時用substr(start, length)C或切片line[start:end]Python把單詞子串也保存下來即可。變體5輸入包含多行直到文件結束EOF。這是OJ常見格式。需要將整個讀取和處理的邏輯包在一個while (getline(cin, line))或for line in sys.stdin:的循環里。每行獨立處理輸出各自的結果。通過這樣一道看似簡單的題目我們實際上深入探討了字符串處理的基石輸入緩沖、字符遍歷、狀態管理、邊界條件處理。這才是學習算法和編程的正確姿勢——不滿足于AC而要理解每一行代碼背后的“所以然”。下次再遇到字符串處理問題不妨先想想我的指針應該怎么走程序現在處于什么狀態